Skip to main content

openlogi_core/config/
per_device.rs

1//! One device's stored settings, a field at a time: the getters and setters
2//! that address a [`DeviceConfig`](super::DeviceConfig) entry by its config key.
3//!
4//! A getter answers for a device with no entry the way a fresh entry would, and
5//! a setter creates the entry it needs. Settings with rules of their own live
6//! beside this file: gesture mode in [`gestures`](super::gestures),
7//! per-application profiles in [`per_app`](super::per_app).
8
9use std::collections::BTreeMap;
10
11use super::{
12    CameraControls, Config, DeviceIdentity, LightSettings, Lighting, ScrollResolution, SmartShift,
13    ThumbwheelSensitivity,
14};
15use crate::binding::{
16    ActionRingConfig, ActionRingIcon, ActionRingSlot, Binding, ButtonId, RingAction,
17};
18use crate::hid::Dpi;
19
20impl Config {
21    /// The bindings stored for `device_key` as they were committed, or an
22    /// empty map when the device has none yet. The effective per-button map,
23    /// with defaults and the per-app overlay applied, is
24    /// [`crate::bindings::bindings_for`].
25    #[must_use]
26    pub fn stored_bindings(&self, device_key: &str) -> BTreeMap<ButtonId, Binding> {
27        self.devices
28            .get(device_key)
29            .map(|d| d.bindings.clone())
30            .unwrap_or_default()
31    }
32
33    /// Records `binding` for `button` on `device_key`, creating the device
34    /// entry if needed. Replaces the whole binding (use
35    /// [`Self::set_gesture_direction`] to edit one direction of a gesture
36    /// binding in place).
37    pub fn set_binding(&mut self, device_key: &str, button: ButtonId, binding: Binding) {
38        self.devices
39            .entry(device_key.to_string())
40            .or_default()
41            .bindings
42            .insert(button, binding);
43    }
44
45    /// Actions Ring settings for `device_key`, falling back to defaults when
46    /// the device has no saved ring configuration.
47    #[must_use]
48    pub fn action_ring(&self, device_key: &str) -> ActionRingConfig {
49        self.devices
50            .get(device_key)
51            .map(|device| device.action_ring.clone())
52            .unwrap_or_default()
53    }
54
55    /// Enable or disable `device_key`'s Actions Ring.
56    pub fn set_action_ring_enabled(&mut self, device_key: &str, enabled: bool) {
57        self.devices
58            .entry(device_key.to_string())
59            .or_default()
60            .action_ring
61            .enabled = enabled;
62    }
63
64    /// Enable or disable ring hover and activation haptics.
65    pub fn set_action_ring_haptics(&mut self, device_key: &str, enabled: bool) {
66        self.devices
67            .entry(device_key.to_string())
68            .or_default()
69            .action_ring
70            .haptics = enabled;
71    }
72
73    /// Replace or clear one slot in the default Actions Ring layout.
74    pub fn set_action_ring_slot(
75        &mut self,
76        device_key: &str,
77        slot: ActionRingSlot,
78        action: Option<RingAction>,
79    ) {
80        self.devices
81            .entry(device_key.to_string())
82            .or_default()
83            .action_ring
84            .default
85            .set_action(slot, action);
86    }
87
88    /// Set or restore the action-derived icon for one default ring slot.
89    pub fn set_action_ring_icon(
90        &mut self,
91        device_key: &str,
92        slot: ActionRingSlot,
93        icon: Option<ActionRingIcon>,
94    ) {
95        self.devices
96            .entry(device_key.to_string())
97            .or_default()
98            .action_ring
99            .default
100            .set_icon(slot, icon);
101    }
102
103    /// The ordered DPI preset list for `device_key`, or an empty `Vec` if the
104    /// device has none configured yet.
105    #[must_use]
106    pub fn dpi_presets(&self, device_key: &str) -> Vec<Dpi> {
107        self.devices
108            .get(device_key)
109            .map(|d| d.dpi_presets.clone())
110            .unwrap_or_default()
111    }
112
113    /// Replace the DPI preset list for `device_key`. Pass an empty `Vec` to
114    /// clear (the device block is kept; the field is just omitted on save
115    /// thanks to `skip_serializing_if`).
116    pub fn set_dpi_presets(&mut self, device_key: &str, presets: Vec<Dpi>) {
117        self.devices
118            .entry(device_key.to_string())
119            .or_default()
120            .dpi_presets = presets;
121    }
122
123    /// The last-known [`DeviceIdentity`] for `device_key`, or `None` if the
124    /// device has never been seen online (or was configured before identities
125    /// were recorded).
126    #[must_use]
127    pub fn device_identity(&self, device_key: &str) -> Option<&DeviceIdentity> {
128        self.devices
129            .get(device_key)
130            .and_then(|d| d.identity.as_ref())
131    }
132
133    /// Record (or refresh) the identity captured for `device_key` while it was
134    /// online, creating the device entry if needed.
135    pub fn set_device_identity(&mut self, device_key: &str, identity: DeviceIdentity) {
136        self.devices
137            .entry(device_key.to_string())
138            .or_default()
139            .identity = Some(identity.without_unit_identifiers());
140    }
141
142    /// Drop everything recorded for `device_key` — identity, custom name, and
143    /// per-device settings. Returns whether an entry existed.
144    pub fn remove_device(&mut self, device_key: &str) -> bool {
145        self.devices.remove(device_key).is_some()
146    }
147
148    /// The user-assigned name for `device_key`, if one is configured.
149    #[must_use]
150    pub fn device_custom_name(&self, device_key: &str) -> Option<&str> {
151        self.devices
152            .get(device_key)
153            .and_then(|device| device.custom_name.as_deref())
154    }
155
156    /// Set the user-assigned name for `device_key`, or clear it to use the
157    /// hardware model name again.
158    pub fn set_device_custom_name(&mut self, device_key: &str, custom_name: Option<String>) {
159        self.devices
160            .entry(device_key.to_string())
161            .or_default()
162            .custom_name = custom_name;
163    }
164
165    /// Iterate every device we've recorded an identity for, as
166    /// `(config_key, identity)`. Used to seed offline placeholder cards so a
167    /// known device stays visible (with its panels) before any live probe.
168    pub fn known_identities(&self) -> impl Iterator<Item = (&str, &DeviceIdentity)> {
169        self.devices
170            .iter()
171            .filter_map(|(k, d)| d.identity.as_ref().map(|i| (k.as_str(), i)))
172    }
173
174    /// The lighting config for `device_key`, or `None` if unset.
175    #[must_use]
176    pub fn lighting(&self, device_key: &str) -> Option<Lighting> {
177        self.devices
178            .get(device_key)
179            .and_then(|d| d.lighting.clone())
180    }
181
182    /// Replace the lighting config for `device_key`.
183    pub fn set_lighting(&mut self, device_key: &str, lighting: Lighting) {
184        self.devices
185            .entry(device_key.to_string())
186            .or_default()
187            .lighting = Some(lighting);
188    }
189
190    /// The saved UVC image controls for `device_key`, or `None` if never set.
191    #[must_use]
192    pub fn camera_controls(&self, device_key: &str) -> Option<CameraControls> {
193        self.devices
194            .get(device_key)
195            .and_then(|d| d.camera_controls.clone())
196    }
197
198    /// Replace the saved UVC image controls for `device_key`.
199    pub fn set_camera_controls(&mut self, device_key: &str, controls: CameraControls) {
200        self.devices
201            .entry(device_key.to_string())
202            .or_default()
203            .camera_controls = Some(controls);
204    }
205
206    /// The saved custom camera profiles for `device_key` (name → snapshot).
207    #[must_use]
208    pub fn camera_profiles(&self, device_key: &str) -> BTreeMap<String, CameraControls> {
209        self.devices
210            .get(device_key)
211            .map(|d| d.camera_profiles.clone())
212            .unwrap_or_default()
213    }
214
215    /// Save (or overwrite) a custom camera profile for `device_key`.
216    pub fn save_camera_profile(&mut self, device_key: &str, name: &str, snap: CameraControls) {
217        self.devices
218            .entry(device_key.to_string())
219            .or_default()
220            .camera_profiles
221            .insert(name.to_string(), snap);
222    }
223
224    /// Delete a custom camera profile, clearing the active selection if it
225    /// named it. Unknown names are a no-op.
226    pub fn delete_camera_profile(&mut self, device_key: &str, name: &str) {
227        if let Some(device) = self.devices.get_mut(device_key) {
228            device.camera_profiles.remove(name);
229            if device.camera_profile.as_deref() == Some(name) {
230                device.camera_profile = None;
231            }
232        }
233    }
234
235    /// The last-applied camera profile name for `device_key`, if any.
236    #[must_use]
237    pub fn camera_active_profile(&self, device_key: &str) -> Option<String> {
238        self.devices
239            .get(device_key)
240            .and_then(|d| d.camera_profile.clone())
241    }
242
243    /// Record which camera profile `device_key` last applied.
244    pub fn set_camera_active_profile(&mut self, device_key: &str, name: Option<String>) {
245        self.devices
246            .entry(device_key.to_string())
247            .or_default()
248            .camera_profile = name;
249    }
250
251    /// The standalone-light config for `device_key`, or `None` if unset.
252    #[must_use]
253    pub fn light(&self, device_key: &str) -> Option<LightSettings> {
254        self.devices.get(device_key).and_then(|d| d.light)
255    }
256
257    /// Replace the standalone-light config for `device_key`.
258    pub fn set_light(&mut self, device_key: &str, light: LightSettings) {
259        self.devices
260            .entry(device_key.to_string())
261            .or_default()
262            .light = Some(light);
263    }
264
265    /// The committed sensor DPI for `device_key`, or `None` if never set.
266    #[must_use]
267    pub fn dpi(&self, device_key: &str) -> Option<Dpi> {
268        self.devices.get(device_key).and_then(|d| d.dpi)
269    }
270
271    /// Record the committed sensor DPI for `device_key`, so the agent can
272    /// re-apply it when the device reconnects (#189).
273    pub fn set_dpi(&mut self, device_key: &str, dpi: Dpi) {
274        self.devices.entry(device_key.to_string()).or_default().dpi = Some(dpi);
275    }
276
277    /// The SmartShift wheel config for `device_key`, or `None` if never set.
278    #[must_use]
279    pub fn smartshift(&self, device_key: &str) -> Option<SmartShift> {
280        self.devices.get(device_key).and_then(|d| d.smartshift)
281    }
282
283    /// The persisted keyboard Fn-lock state for `device_key`, or `None` when
284    /// the user never set one (the keyboard keeps its own state).
285    #[must_use]
286    pub fn fn_lock(&self, device_key: &str) -> Option<bool> {
287        self.devices.get(device_key).and_then(|d| d.fn_lock)
288    }
289
290    /// Record the SmartShift wheel config for `device_key`, so the agent can
291    /// re-apply it when the device reconnects (#189).
292    pub fn set_smartshift(&mut self, device_key: &str, smartshift: SmartShift) {
293        self.devices
294            .entry(device_key.to_string())
295            .or_default()
296            .smartshift = Some(smartshift);
297    }
298
299    /// Whether `device_key`'s scroll wheel is inverted (issue #126). `false`
300    /// (the native direction) for an unconfigured or absent device.
301    #[must_use]
302    pub fn invert_scroll(&self, device_key: &str) -> bool {
303        self.devices
304            .get(device_key)
305            .is_some_and(|d| d.invert_scroll)
306    }
307
308    /// Set whether `device_key`'s scroll wheel is inverted. The agent reads this
309    /// on the next `ReloadConfig` and applies it in the OS hook.
310    pub fn set_invert_scroll(&mut self, device_key: &str, invert: bool) {
311        self.devices
312            .entry(device_key.to_string())
313            .or_default()
314            .invert_scroll = invert;
315    }
316
317    /// The configured wheel resolution for `device_key`, or `None` when
318    /// OpenLogi should leave the device's current resolution unchanged.
319    #[must_use]
320    pub fn scroll_resolution(&self, device_key: &str) -> Option<ScrollResolution> {
321        self.devices
322            .get(device_key)
323            .and_then(|device| device.scroll_resolution)
324    }
325
326    /// Set the wheel resolution OpenLogi should restore for `device_key`.
327    /// Passing `None` returns the device to its unmanaged default state.
328    pub fn set_scroll_resolution(
329        &mut self,
330        device_key: &str,
331        resolution: Option<ScrollResolution>,
332    ) {
333        self.devices
334            .entry(device_key.to_string())
335            .or_default()
336            .scroll_resolution = resolution;
337    }
338
339    /// Whether OpenLogi manages `device_key` at all (capture + volatile
340    /// re-apply). Unconfigured devices are managed.
341    #[must_use]
342    pub fn device_enabled(&self, device_key: &str) -> bool {
343        self.devices.get(device_key).is_none_or(|d| d.enabled)
344    }
345
346    /// Enable or disable OpenLogi's management of `device_key`.
347    pub fn set_device_enabled(&mut self, device_key: &str, enabled: bool) {
348        self.devices
349            .entry(device_key.to_string())
350            .or_default()
351            .enabled = enabled;
352    }
353
354    /// The effective thumb-wheel sensitivity for `device_key`: the device's
355    /// override when set, else the app-wide default.
356    #[must_use]
357    pub fn thumbwheel_sensitivity(&self, device_key: &str) -> ThumbwheelSensitivity {
358        self.devices
359            .get(device_key)
360            .and_then(|d| d.thumbwheel_sensitivity)
361            .unwrap_or(self.app_settings.thumbwheel_sensitivity)
362    }
363
364    /// Set (or clear, with `None`) `device_key`'s thumb-wheel sensitivity
365    /// override.
366    pub fn set_device_thumbwheel_sensitivity(
367        &mut self,
368        device_key: &str,
369        sensitivity: Option<ThumbwheelSensitivity>,
370    ) {
371        self.devices
372            .entry(device_key.to_string())
373            .or_default()
374            .thumbwheel_sensitivity = sensitivity;
375    }
376}