Skip to main content

zmk_studio_api/
client.rs

1use std::collections::{HashMap, VecDeque};
2use std::io::{Read, Write};
3
4use crate::binding::{Behavior, BehaviorRole, role_from_display_name};
5use crate::framing::FrameDecoder;
6use crate::hid_usage::HidUsage;
7use crate::proto::zmk;
8use crate::proto::zmk::studio;
9use crate::protocol::{ProtocolError, decode_responses, encode_request};
10#[cfg(feature = "ble")]
11use crate::transport::ble::{BleTransport, BleTransportError};
12#[cfg(feature = "serial")]
13use crate::transport::serial::{SerialTransport, SerialTransportError};
14
15/// High-level error type returned by [`StudioClient`] operations.
16#[derive(Debug)]
17pub enum ClientError {
18    Io(std::io::Error),
19    Protocol(ProtocolError),
20    Meta(zmk::meta::ErrorConditions),
21    NoResponse,
22    MissingResponseType,
23    MissingSubsystem,
24    UnexpectedSubsystem(&'static str),
25    UnexpectedRequestId { expected: u32, actual: u32 },
26    UnknownEnumValue { field: &'static str, value: i32 },
27    SetLayerBindingFailed(zmk::keymap::SetLayerBindingResponse),
28    SaveChangesFailed(zmk::keymap::SaveChangesErrorCode),
29    SetActivePhysicalLayoutFailed(zmk::keymap::SetActivePhysicalLayoutErrorCode),
30    MoveLayerFailed(zmk::keymap::MoveLayerErrorCode),
31    AddLayerFailed(zmk::keymap::AddLayerErrorCode),
32    RemoveLayerFailed(zmk::keymap::RemoveLayerErrorCode),
33    RestoreLayerFailed(zmk::keymap::RestoreLayerErrorCode),
34    SetLayerPropsFailed(zmk::keymap::SetLayerPropsResponse),
35    InvalidLayerOrPosition { layer_id: u32, key_position: i32 },
36    MissingBehaviorRole(&'static str),
37    BehaviorIdOutOfRange { behavior_id: u32 },
38}
39
40impl std::fmt::Display for ClientError {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self {
43            Self::Io(err) => write!(f, "I/O error: {err}"),
44            Self::Protocol(err) => write!(f, "Protocol error: {err}"),
45            Self::Meta(cond) => write!(f, "Device returned meta error: {}", cond.as_str_name()),
46            Self::NoResponse => write!(f, "Device returned no response"),
47            Self::MissingResponseType => write!(f, "Response was missing type"),
48            Self::MissingSubsystem => write!(f, "Request response was missing subsystem"),
49            Self::UnexpectedSubsystem(expected) => {
50                write!(f, "Unexpected subsystem in response; expected {expected}")
51            }
52            Self::UnexpectedRequestId { expected, actual } => {
53                write!(
54                    f,
55                    "Unexpected request ID in response: expected {expected}, got {actual}"
56                )
57            }
58            Self::UnknownEnumValue { field, value } => {
59                write!(f, "Unknown enum value for {field}: {value}")
60            }
61            Self::SetLayerBindingFailed(code) => {
62                write!(f, "Set layer binding failed: {}", code.as_str_name())
63            }
64            Self::SaveChangesFailed(code) => {
65                write!(f, "Save changes failed: {}", code.as_str_name())
66            }
67            Self::SetActivePhysicalLayoutFailed(code) => {
68                write!(
69                    f,
70                    "Set active physical layout failed: {}",
71                    code.as_str_name()
72                )
73            }
74            Self::MoveLayerFailed(code) => write!(f, "Move layer failed: {}", code.as_str_name()),
75            Self::AddLayerFailed(code) => write!(f, "Add layer failed: {}", code.as_str_name()),
76            Self::RemoveLayerFailed(code) => {
77                write!(f, "Remove layer failed: {}", code.as_str_name())
78            }
79            Self::RestoreLayerFailed(code) => {
80                write!(f, "Restore layer failed: {}", code.as_str_name())
81            }
82            Self::SetLayerPropsFailed(code) => {
83                write!(f, "Set layer properties failed: {}", code.as_str_name())
84            }
85            Self::InvalidLayerOrPosition {
86                layer_id,
87                key_position,
88            } => write!(
89                f,
90                "Invalid layer/position: layer_id={layer_id}, key_position={key_position}"
91            ),
92            Self::MissingBehaviorRole(role) => {
93                write!(f, "Missing required behavior role in firmware: {role}")
94            }
95            Self::BehaviorIdOutOfRange { behavior_id } => {
96                write!(f, "Behavior ID is out of i32 range: {behavior_id}")
97            }
98        }
99    }
100}
101
102impl std::error::Error for ClientError {
103    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
104        match self {
105            Self::Io(err) => Some(err),
106            Self::Protocol(err) => Some(err),
107            _ => None,
108        }
109    }
110}
111
112impl From<std::io::Error> for ClientError {
113    fn from(value: std::io::Error) -> Self {
114        Self::Io(value)
115    }
116}
117
118impl From<ProtocolError> for ClientError {
119    fn from(value: ProtocolError) -> Self {
120        Self::Protocol(value)
121    }
122}
123
124/// High-level synchronous ZMK Studio RPC client.
125///
126/// The generic parameter `T` is any transport implementing [`Read`] + [`Write`]
127/// (for example [`crate::transport::serial::SerialTransport`]).
128pub struct StudioClient<T> {
129    io: T,
130    next_request_id: u32,
131    decoder: FrameDecoder,
132    read_buffer: Vec<u8>,
133    responses: VecDeque<studio::Response>,
134    notifications: VecDeque<studio::Notification>,
135    behavior_role_by_id: HashMap<u32, BehaviorRole>,
136    behavior_id_by_role: HashMap<BehaviorRole, u32>,
137}
138
139impl<T: Read + Write> StudioClient<T> {
140    pub fn new(io: T) -> Self {
141        Self::with_read_buffer(io, 256)
142    }
143
144    fn with_read_buffer(io: T, read_buffer_size: usize) -> Self {
145        Self {
146            io,
147            next_request_id: 0,
148            decoder: FrameDecoder::new(),
149            read_buffer: vec![0; read_buffer_size.max(1)],
150            responses: VecDeque::new(),
151            notifications: VecDeque::new(),
152            behavior_role_by_id: HashMap::new(),
153            behavior_id_by_role: HashMap::new(),
154        }
155    }
156
157    /// Returns the next queued notification, if any.
158    pub fn next_notification(&mut self) -> Option<studio::Notification> {
159        self.notifications.pop_front()
160    }
161
162    /// Blocks until a notification arrives and returns it.
163    pub fn read_notification_blocking(&mut self) -> Result<studio::Notification, ClientError> {
164        loop {
165            if let Some(notification) = self.next_notification() {
166                return Ok(notification);
167            }
168
169            let _ = self.read_next_response()?;
170        }
171    }
172
173    /// Returns static device information.
174    pub fn get_device_info(&mut self) -> Result<zmk::core::GetDeviceInfoResponse, ClientError> {
175        let response = self.call_core(zmk::core::request::RequestType::GetDeviceInfo(true))?;
176        match response.response_type {
177            Some(zmk::core::response::ResponseType::GetDeviceInfo(info)) => Ok(info),
178            _ => Err(ClientError::MissingResponseType),
179        }
180    }
181
182    /// Returns the current Studio lock state.
183    pub fn get_lock_state(&mut self) -> Result<zmk::core::LockState, ClientError> {
184        let response = self.call_core(zmk::core::request::RequestType::GetLockState(true))?;
185        match response.response_type {
186            Some(zmk::core::response::ResponseType::GetLockState(state)) => {
187                zmk::core::LockState::try_from(state).map_err(|_| ClientError::UnknownEnumValue {
188                    field: "core.get_lock_state",
189                    value: state,
190                })
191            }
192            _ => Err(ClientError::MissingResponseType),
193        }
194    }
195
196    /// Resets settings on the device.
197    ///
198    /// Returns the firmware-provided success boolean.
199    pub fn reset_settings(&mut self) -> Result<bool, ClientError> {
200        let response = self.call_core(zmk::core::request::RequestType::ResetSettings(true))?;
201        match response.response_type {
202            Some(zmk::core::response::ResponseType::ResetSettings(ok)) => Ok(ok),
203            _ => Err(ClientError::MissingResponseType),
204        }
205    }
206
207    /// Lists behavior IDs available on the connected device.
208    pub fn list_all_behaviors(&mut self) -> Result<Vec<u32>, ClientError> {
209        let response =
210            self.call_behaviors(zmk::behaviors::request::RequestType::ListAllBehaviors(true))?;
211        match response.response_type {
212            Some(zmk::behaviors::response::ResponseType::ListAllBehaviors(items)) => {
213                Ok(items.behaviors)
214            }
215            _ => Err(ClientError::MissingResponseType),
216        }
217    }
218
219    /// Returns details for a behavior ID (name and parameter metadata).
220    pub fn get_behavior_details(
221        &mut self,
222        behavior_id: u32,
223    ) -> Result<zmk::behaviors::GetBehaviorDetailsResponse, ClientError> {
224        let request = zmk::behaviors::GetBehaviorDetailsRequest { behavior_id };
225        let response = self.call_behaviors(
226            zmk::behaviors::request::RequestType::GetBehaviorDetails(request),
227        )?;
228        match response.response_type {
229            Some(zmk::behaviors::response::ResponseType::GetBehaviorDetails(details)) => {
230                Ok(details)
231            }
232            _ => Err(ClientError::MissingResponseType),
233        }
234    }
235
236    /// Returns the current keymap state from the device.
237    pub fn get_keymap(&mut self) -> Result<zmk::keymap::Keymap, ClientError> {
238        let response = self.call_keymap(zmk::keymap::request::RequestType::GetKeymap(true))?;
239        match response.response_type {
240            Some(zmk::keymap::response::ResponseType::GetKeymap(keymap)) => Ok(keymap),
241            _ => Err(ClientError::MissingResponseType),
242        }
243    }
244
245    /// Returns available physical layouts and the active layout index.
246    pub fn get_physical_layouts(&mut self) -> Result<zmk::keymap::PhysicalLayouts, ClientError> {
247        let response =
248            self.call_keymap(zmk::keymap::request::RequestType::GetPhysicalLayouts(true))?;
249        match response.response_type {
250            Some(zmk::keymap::response::ResponseType::GetPhysicalLayouts(layouts)) => Ok(layouts),
251            _ => Err(ClientError::MissingResponseType),
252        }
253    }
254
255    /// Sets a raw behavior binding for a specific layer position.
256    pub fn set_layer_binding(
257        &mut self,
258        layer_id: u32,
259        key_position: i32,
260        binding: zmk::keymap::BehaviorBinding,
261    ) -> Result<(), ClientError> {
262        let request = zmk::keymap::SetLayerBindingRequest {
263            layer_id,
264            key_position,
265            binding: Some(binding),
266        };
267
268        let response =
269            self.call_keymap(zmk::keymap::request::RequestType::SetLayerBinding(request))?;
270
271        match response.response_type {
272            Some(zmk::keymap::response::ResponseType::SetLayerBinding(raw)) => {
273                let code = zmk::keymap::SetLayerBindingResponse::try_from(raw).map_err(|_| {
274                    ClientError::UnknownEnumValue {
275                        field: "keymap.set_layer_binding",
276                        value: raw,
277                    }
278                })?;
279
280                if code == zmk::keymap::SetLayerBindingResponse::SetLayerBindingRespOk {
281                    Ok(())
282                } else {
283                    Err(ClientError::SetLayerBindingFailed(code))
284                }
285            }
286            _ => Err(ClientError::MissingResponseType),
287        }
288    }
289
290    /// Reads a behavior from a specific layer/key position.
291    pub fn get_key_at(
292        &mut self,
293        layer_id: u32,
294        key_position: i32,
295    ) -> Result<Behavior, ClientError> {
296        self.ensure_behavior_catalog()?;
297
298        let keymap = self.get_keymap()?;
299        let binding = binding_at(&keymap, layer_id, key_position).ok_or(
300            ClientError::InvalidLayerOrPosition {
301                layer_id,
302                key_position,
303            },
304        )?;
305
306        Ok(self.resolve_binding(&binding))
307    }
308
309    /// Fetches the keymap and resolves every binding into a typed [`Behavior`].
310    ///
311    /// Returns a `Vec` of layers, each layer being a `Vec<Behavior>` matching
312    /// the order of bindings in the keymap. It fetches the keymap once and converts all
313    /// bindings in a single pass.
314    pub fn resolve_keymap(&mut self) -> Result<Vec<Vec<Behavior>>, ClientError> {
315        self.ensure_behavior_catalog()?;
316        let keymap = self.get_keymap()?;
317
318        let layers = keymap
319            .layers
320            .iter()
321            .map(|layer| {
322                layer
323                    .bindings
324                    .iter()
325                    .map(|binding| self.resolve_binding(binding))
326                    .collect()
327            })
328            .collect();
329
330        Ok(layers)
331    }
332
333    fn resolve_binding(&self, binding: &zmk::keymap::BehaviorBinding) -> Behavior {
334        let Ok(binding_behavior_id) = u32::try_from(binding.behavior_id) else {
335            return Behavior::Unknown {
336                behavior_id: binding.behavior_id,
337                param1: binding.param1,
338                param2: binding.param2,
339            };
340        };
341        let Some(role) = self.behavior_role_by_id.get(&binding_behavior_id).copied() else {
342            return Behavior::Unknown {
343                behavior_id: binding.behavior_id,
344                param1: binding.param1,
345                param2: binding.param2,
346            };
347        };
348
349        match role {
350            BehaviorRole::KeyPress => Behavior::KeyPress(HidUsage::from_encoded(binding.param1)),
351            BehaviorRole::KeyToggle => Behavior::KeyToggle(HidUsage::from_encoded(binding.param1)),
352            BehaviorRole::LayerTap => Behavior::LayerTap {
353                layer_id: binding.param1,
354                tap: HidUsage::from_encoded(binding.param2),
355            },
356            BehaviorRole::ModTap => Behavior::ModTap {
357                hold: HidUsage::from_encoded(binding.param1),
358                tap: HidUsage::from_encoded(binding.param2),
359            },
360            BehaviorRole::StickyKey => Behavior::StickyKey(HidUsage::from_encoded(binding.param1)),
361            BehaviorRole::StickyLayer => Behavior::StickyLayer {
362                layer_id: binding.param1,
363            },
364            BehaviorRole::MomentaryLayer => Behavior::MomentaryLayer {
365                layer_id: binding.param1,
366            },
367            BehaviorRole::ToggleLayer => Behavior::ToggleLayer {
368                layer_id: binding.param1,
369            },
370            BehaviorRole::ToLayer => Behavior::ToLayer {
371                layer_id: binding.param1,
372            },
373            BehaviorRole::Bluetooth => Behavior::Bluetooth {
374                command: binding.param1,
375                value: binding.param2,
376            },
377            BehaviorRole::ExternalPower => Behavior::ExternalPower {
378                value: binding.param1,
379            },
380            BehaviorRole::OutputSelection => Behavior::OutputSelection {
381                value: binding.param1,
382            },
383            BehaviorRole::Backlight => Behavior::Backlight {
384                command: binding.param1,
385                value: binding.param2,
386            },
387            BehaviorRole::Underglow => Behavior::Underglow {
388                command: binding.param1,
389                value: binding.param2,
390            },
391            BehaviorRole::MouseKeyPress => Behavior::MouseKeyPress {
392                value: binding.param1,
393            },
394            BehaviorRole::MouseMove => Behavior::MouseMove {
395                value: binding.param1,
396            },
397            BehaviorRole::MouseScroll => Behavior::MouseScroll {
398                value: binding.param1,
399            },
400            BehaviorRole::CapsWord => Behavior::CapsWord,
401            BehaviorRole::KeyRepeat => Behavior::KeyRepeat,
402            BehaviorRole::Reset => Behavior::Reset,
403            BehaviorRole::Bootloader => Behavior::Bootloader,
404            BehaviorRole::SoftOff => Behavior::SoftOff,
405            BehaviorRole::StudioUnlock => Behavior::StudioUnlock,
406            BehaviorRole::GraveEscape => Behavior::GraveEscape,
407            BehaviorRole::Transparent => Behavior::Transparent,
408            BehaviorRole::None => Behavior::None,
409        }
410    }
411
412    /// Set a behavior at a specific layer/key position.
413    ///
414    /// Persist with [`StudioClient::save_changes`] or revert with [`StudioClient::discard_changes`].
415    pub fn set_key_at(
416        &mut self,
417        layer_id: u32,
418        key_position: i32,
419        behavior: Behavior,
420    ) -> Result<(), ClientError> {
421        self.ensure_behavior_catalog()?;
422        let binding = match behavior {
423            Behavior::KeyPress(key) => zmk::keymap::BehaviorBinding {
424                behavior_id: self.behavior_id_for(BehaviorRole::KeyPress, "Key Press")?,
425                param1: key.to_hid_usage(),
426                param2: 0,
427            },
428            Behavior::KeyToggle(key) => zmk::keymap::BehaviorBinding {
429                behavior_id: self.behavior_id_for(BehaviorRole::KeyToggle, "Key Toggle")?,
430                param1: key.to_hid_usage(),
431                param2: 0,
432            },
433            Behavior::LayerTap {
434                layer_id: hold_layer_id,
435                tap,
436            } => zmk::keymap::BehaviorBinding {
437                behavior_id: self.behavior_id_for(BehaviorRole::LayerTap, "Layer-Tap")?,
438                param1: hold_layer_id,
439                param2: tap.to_hid_usage(),
440            },
441            Behavior::ModTap { hold, tap } => zmk::keymap::BehaviorBinding {
442                behavior_id: self.behavior_id_for(BehaviorRole::ModTap, "Mod-Tap")?,
443                param1: hold.to_hid_usage(),
444                param2: tap.to_hid_usage(),
445            },
446            Behavior::StickyKey(key) => zmk::keymap::BehaviorBinding {
447                behavior_id: self.behavior_id_for(BehaviorRole::StickyKey, "Sticky Key")?,
448                param1: key.to_hid_usage(),
449                param2: 0,
450            },
451            Behavior::StickyLayer {
452                layer_id: target_layer_id,
453            } => zmk::keymap::BehaviorBinding {
454                behavior_id: self.behavior_id_for(BehaviorRole::StickyLayer, "Sticky Layer")?,
455                param1: target_layer_id,
456                param2: 0,
457            },
458            Behavior::MomentaryLayer {
459                layer_id: hold_layer_id,
460            } => zmk::keymap::BehaviorBinding {
461                behavior_id: self
462                    .behavior_id_for(BehaviorRole::MomentaryLayer, "Momentary Layer")?,
463                param1: hold_layer_id,
464                param2: 0,
465            },
466            Behavior::ToggleLayer {
467                layer_id: target_layer_id,
468            } => zmk::keymap::BehaviorBinding {
469                behavior_id: self.behavior_id_for(BehaviorRole::ToggleLayer, "Toggle Layer")?,
470                param1: target_layer_id,
471                param2: 0,
472            },
473            Behavior::ToLayer {
474                layer_id: target_layer_id,
475            } => zmk::keymap::BehaviorBinding {
476                behavior_id: self.behavior_id_for(BehaviorRole::ToLayer, "To Layer")?,
477                param1: target_layer_id,
478                param2: 0,
479            },
480            Behavior::Bluetooth { command, value } => zmk::keymap::BehaviorBinding {
481                behavior_id: self.behavior_id_for(BehaviorRole::Bluetooth, "Bluetooth")?,
482                param1: command,
483                param2: value,
484            },
485            Behavior::ExternalPower { value } => zmk::keymap::BehaviorBinding {
486                behavior_id: self.behavior_id_for(BehaviorRole::ExternalPower, "External Power")?,
487                param1: value,
488                param2: 0,
489            },
490            Behavior::OutputSelection { value } => zmk::keymap::BehaviorBinding {
491                behavior_id: self
492                    .behavior_id_for(BehaviorRole::OutputSelection, "Output Selection")?,
493                param1: value,
494                param2: 0,
495            },
496            Behavior::Backlight { command, value } => zmk::keymap::BehaviorBinding {
497                behavior_id: self.behavior_id_for(BehaviorRole::Backlight, "Backlight")?,
498                param1: command,
499                param2: value,
500            },
501            Behavior::Underglow { command, value } => zmk::keymap::BehaviorBinding {
502                behavior_id: self.behavior_id_for(BehaviorRole::Underglow, "Underglow")?,
503                param1: command,
504                param2: value,
505            },
506            Behavior::MouseKeyPress { value } => zmk::keymap::BehaviorBinding {
507                behavior_id: self
508                    .behavior_id_for(BehaviorRole::MouseKeyPress, "Mouse Key Press")?,
509                param1: value,
510                param2: 0,
511            },
512            Behavior::MouseMove { value } => zmk::keymap::BehaviorBinding {
513                behavior_id: self.behavior_id_for(BehaviorRole::MouseMove, "Mouse Move")?,
514                param1: value,
515                param2: 0,
516            },
517            Behavior::MouseScroll { value } => zmk::keymap::BehaviorBinding {
518                behavior_id: self.behavior_id_for(BehaviorRole::MouseScroll, "Mouse Scroll")?,
519                param1: value,
520                param2: 0,
521            },
522            Behavior::CapsWord => zmk::keymap::BehaviorBinding {
523                behavior_id: self.behavior_id_for(BehaviorRole::CapsWord, "Caps Word")?,
524                param1: 0,
525                param2: 0,
526            },
527            Behavior::KeyRepeat => zmk::keymap::BehaviorBinding {
528                behavior_id: self.behavior_id_for(BehaviorRole::KeyRepeat, "Key Repeat")?,
529                param1: 0,
530                param2: 0,
531            },
532            Behavior::Reset => zmk::keymap::BehaviorBinding {
533                behavior_id: self.behavior_id_for(BehaviorRole::Reset, "Reset")?,
534                param1: 0,
535                param2: 0,
536            },
537            Behavior::Bootloader => zmk::keymap::BehaviorBinding {
538                behavior_id: self.behavior_id_for(BehaviorRole::Bootloader, "Bootloader")?,
539                param1: 0,
540                param2: 0,
541            },
542            Behavior::SoftOff => zmk::keymap::BehaviorBinding {
543                behavior_id: self.behavior_id_for(BehaviorRole::SoftOff, "Soft Off")?,
544                param1: 0,
545                param2: 0,
546            },
547            Behavior::StudioUnlock => zmk::keymap::BehaviorBinding {
548                behavior_id: self.behavior_id_for(BehaviorRole::StudioUnlock, "Studio Unlock")?,
549                param1: 0,
550                param2: 0,
551            },
552            Behavior::GraveEscape => zmk::keymap::BehaviorBinding {
553                behavior_id: self.behavior_id_for(BehaviorRole::GraveEscape, "Grave/Escape")?,
554                param1: 0,
555                param2: 0,
556            },
557            Behavior::Transparent => zmk::keymap::BehaviorBinding {
558                behavior_id: self.behavior_id_for(BehaviorRole::Transparent, "Transparent")?,
559                param1: 0,
560                param2: 0,
561            },
562            Behavior::None => zmk::keymap::BehaviorBinding {
563                behavior_id: self.behavior_id_for(BehaviorRole::None, "None")?,
564                param1: 0,
565                param2: 0,
566            },
567            Behavior::Unknown {
568                behavior_id,
569                param1,
570                param2,
571            } => zmk::keymap::BehaviorBinding {
572                behavior_id,
573                param1,
574                param2,
575            },
576        };
577
578        self.set_layer_binding(layer_id, key_position, binding)
579    }
580
581    /// Returns whether there are pending unsaved keymap/layout changes.
582    pub fn check_unsaved_changes(&mut self) -> Result<bool, ClientError> {
583        let response =
584            self.call_keymap(zmk::keymap::request::RequestType::CheckUnsavedChanges(true))?;
585        match response.response_type {
586            Some(zmk::keymap::response::ResponseType::CheckUnsavedChanges(has_changes)) => {
587                Ok(has_changes)
588            }
589            _ => Err(ClientError::MissingResponseType),
590        }
591    }
592
593    /// Saves pending keymap/layout mutations made by methods like [`StudioClient::set_key_at`].
594    ///
595    /// After this succeeds, changes are persisted on the device.
596    pub fn save_changes(&mut self) -> Result<(), ClientError> {
597        let response = self.call_keymap(zmk::keymap::request::RequestType::SaveChanges(true))?;
598        match response.response_type {
599            Some(zmk::keymap::response::ResponseType::SaveChanges(save)) => match save.result {
600                Some(zmk::keymap::save_changes_response::Result::Ok(_)) => Ok(()),
601                Some(zmk::keymap::save_changes_response::Result::Err(raw)) => {
602                    let err = zmk::keymap::SaveChangesErrorCode::try_from(raw).map_err(|_| {
603                        ClientError::UnknownEnumValue {
604                            field: "keymap.save_changes",
605                            value: raw,
606                        }
607                    })?;
608                    Err(ClientError::SaveChangesFailed(err))
609                }
610                None => Err(ClientError::MissingResponseType),
611            },
612            _ => Err(ClientError::MissingResponseType),
613        }
614    }
615
616    /// Discards pending keymap/layout mutations made since the last save.
617    ///
618    /// Returns `true` if there were pending changes and they were discarded.
619    pub fn discard_changes(&mut self) -> Result<bool, ClientError> {
620        let response = self.call_keymap(zmk::keymap::request::RequestType::DiscardChanges(true))?;
621        match response.response_type {
622            Some(zmk::keymap::response::ResponseType::DiscardChanges(discarded)) => Ok(discarded),
623            _ => Err(ClientError::MissingResponseType),
624        }
625    }
626
627    /// Sets the active physical layout by index and returns the resulting keymap.
628    pub fn set_active_physical_layout(
629        &mut self,
630        index: u32,
631    ) -> Result<zmk::keymap::Keymap, ClientError> {
632        let response = self.call_keymap(
633            zmk::keymap::request::RequestType::SetActivePhysicalLayout(index),
634        )?;
635        match response.response_type {
636            Some(zmk::keymap::response::ResponseType::SetActivePhysicalLayout(resp)) => {
637                match resp.result {
638                    Some(zmk::keymap::set_active_physical_layout_response::Result::Ok(keymap)) => {
639                        Ok(keymap)
640                    }
641                    Some(zmk::keymap::set_active_physical_layout_response::Result::Err(raw)) => {
642                        let err = zmk::keymap::SetActivePhysicalLayoutErrorCode::try_from(raw)
643                            .map_err(|_| ClientError::UnknownEnumValue {
644                                field: "keymap.set_active_physical_layout",
645                                value: raw,
646                            })?;
647                        Err(ClientError::SetActivePhysicalLayoutFailed(err))
648                    }
649                    None => Err(ClientError::MissingResponseType),
650                }
651            }
652            _ => Err(ClientError::MissingResponseType),
653        }
654    }
655
656    /// Moves a layer from `start_index` to `dest_index` and returns the updated keymap.
657    pub fn move_layer(
658        &mut self,
659        start_index: u32,
660        dest_index: u32,
661    ) -> Result<zmk::keymap::Keymap, ClientError> {
662        let request = zmk::keymap::MoveLayerRequest {
663            start_index,
664            dest_index,
665        };
666        let response = self.call_keymap(zmk::keymap::request::RequestType::MoveLayer(request))?;
667        match response.response_type {
668            Some(zmk::keymap::response::ResponseType::MoveLayer(resp)) => match resp.result {
669                Some(zmk::keymap::move_layer_response::Result::Ok(keymap)) => Ok(keymap),
670                Some(zmk::keymap::move_layer_response::Result::Err(raw)) => {
671                    let err = zmk::keymap::MoveLayerErrorCode::try_from(raw).map_err(|_| {
672                        ClientError::UnknownEnumValue {
673                            field: "keymap.move_layer",
674                            value: raw,
675                        }
676                    })?;
677                    Err(ClientError::MoveLayerFailed(err))
678                }
679                None => Err(ClientError::MissingResponseType),
680            },
681            _ => Err(ClientError::MissingResponseType),
682        }
683    }
684
685    /// Adds a layer and returns firmware-provided details about the created layer.
686    pub fn add_layer(&mut self) -> Result<zmk::keymap::AddLayerResponseDetails, ClientError> {
687        let response = self.call_keymap(zmk::keymap::request::RequestType::AddLayer(
688            zmk::keymap::AddLayerRequest {},
689        ))?;
690        match response.response_type {
691            Some(zmk::keymap::response::ResponseType::AddLayer(resp)) => match resp.result {
692                Some(zmk::keymap::add_layer_response::Result::Ok(details)) => Ok(details),
693                Some(zmk::keymap::add_layer_response::Result::Err(raw)) => {
694                    let err = zmk::keymap::AddLayerErrorCode::try_from(raw).map_err(|_| {
695                        ClientError::UnknownEnumValue {
696                            field: "keymap.add_layer",
697                            value: raw,
698                        }
699                    })?;
700                    Err(ClientError::AddLayerFailed(err))
701                }
702                None => Err(ClientError::MissingResponseType),
703            },
704            _ => Err(ClientError::MissingResponseType),
705        }
706    }
707
708    /// Removes a layer by index.
709    pub fn remove_layer(&mut self, layer_index: u32) -> Result<(), ClientError> {
710        let request = zmk::keymap::RemoveLayerRequest { layer_index };
711        let response = self.call_keymap(zmk::keymap::request::RequestType::RemoveLayer(request))?;
712        match response.response_type {
713            Some(zmk::keymap::response::ResponseType::RemoveLayer(resp)) => match resp.result {
714                Some(zmk::keymap::remove_layer_response::Result::Ok(_)) => Ok(()),
715                Some(zmk::keymap::remove_layer_response::Result::Err(raw)) => {
716                    let err = zmk::keymap::RemoveLayerErrorCode::try_from(raw).map_err(|_| {
717                        ClientError::UnknownEnumValue {
718                            field: "keymap.remove_layer",
719                            value: raw,
720                        }
721                    })?;
722                    Err(ClientError::RemoveLayerFailed(err))
723                }
724                None => Err(ClientError::MissingResponseType),
725            },
726            _ => Err(ClientError::MissingResponseType),
727        }
728    }
729
730    /// Restores a previously removed layer at a specific index.
731    pub fn restore_layer(
732        &mut self,
733        layer_id: u32,
734        at_index: u32,
735    ) -> Result<zmk::keymap::Layer, ClientError> {
736        let request = zmk::keymap::RestoreLayerRequest { layer_id, at_index };
737        let response =
738            self.call_keymap(zmk::keymap::request::RequestType::RestoreLayer(request))?;
739        match response.response_type {
740            Some(zmk::keymap::response::ResponseType::RestoreLayer(resp)) => match resp.result {
741                Some(zmk::keymap::restore_layer_response::Result::Ok(layer)) => Ok(layer),
742                Some(zmk::keymap::restore_layer_response::Result::Err(raw)) => {
743                    let err = zmk::keymap::RestoreLayerErrorCode::try_from(raw).map_err(|_| {
744                        ClientError::UnknownEnumValue {
745                            field: "keymap.restore_layer",
746                            value: raw,
747                        }
748                    })?;
749                    Err(ClientError::RestoreLayerFailed(err))
750                }
751                None => Err(ClientError::MissingResponseType),
752            },
753            _ => Err(ClientError::MissingResponseType),
754        }
755    }
756
757    /// Sets user-facing properties for a layer (currently just `name`).
758    pub fn set_layer_props(
759        &mut self,
760        layer_id: u32,
761        name: impl Into<String>,
762    ) -> Result<(), ClientError> {
763        let request = zmk::keymap::SetLayerPropsRequest {
764            layer_id,
765            name: name.into(),
766        };
767        let response =
768            self.call_keymap(zmk::keymap::request::RequestType::SetLayerProps(request))?;
769        match response.response_type {
770            Some(zmk::keymap::response::ResponseType::SetLayerProps(raw)) => {
771                let code = zmk::keymap::SetLayerPropsResponse::try_from(raw).map_err(|_| {
772                    ClientError::UnknownEnumValue {
773                        field: "keymap.set_layer_props",
774                        value: raw,
775                    }
776                })?;
777
778                if code == zmk::keymap::SetLayerPropsResponse::SetLayerPropsRespOk {
779                    Ok(())
780                } else {
781                    Err(ClientError::SetLayerPropsFailed(code))
782                }
783            }
784            _ => Err(ClientError::MissingResponseType),
785        }
786    }
787
788    fn behavior_id_for(
789        &self,
790        role: BehaviorRole,
791        display_name: &'static str,
792    ) -> Result<i32, ClientError> {
793        let behavior_id = self
794            .behavior_id_by_role
795            .get(&role)
796            .copied()
797            .ok_or(ClientError::MissingBehaviorRole(display_name))?;
798        i32::try_from(behavior_id).map_err(|_| ClientError::BehaviorIdOutOfRange { behavior_id })
799    }
800
801    fn ensure_behavior_catalog(&mut self) -> Result<(), ClientError> {
802        if !self.behavior_role_by_id.is_empty() {
803            return Ok(());
804        }
805
806        let ids = self.list_all_behaviors()?;
807        for id in ids {
808            let details = self.get_behavior_details(id)?;
809            let role = role_from_display_name(&details.display_name);
810            if let Some(role) = role {
811                self.behavior_role_by_id.insert(id, role);
812                self.behavior_id_by_role.entry(role).or_insert(id);
813            }
814        }
815
816        Ok(())
817    }
818
819    fn call_core(
820        &mut self,
821        request_type: zmk::core::request::RequestType,
822    ) -> Result<zmk::core::Response, ClientError> {
823        let request = zmk::core::Request {
824            request_type: Some(request_type),
825        };
826        let rr = self.call(studio::request::Subsystem::Core(request))?;
827
828        match rr.subsystem {
829            Some(studio::request_response::Subsystem::Core(resp)) => Ok(resp),
830            Some(_) => Err(ClientError::UnexpectedSubsystem("core")),
831            None => Err(ClientError::MissingSubsystem),
832        }
833    }
834
835    fn call_behaviors(
836        &mut self,
837        request_type: zmk::behaviors::request::RequestType,
838    ) -> Result<zmk::behaviors::Response, ClientError> {
839        let request = zmk::behaviors::Request {
840            request_type: Some(request_type),
841        };
842        let rr = self.call(studio::request::Subsystem::Behaviors(request))?;
843
844        match rr.subsystem {
845            Some(studio::request_response::Subsystem::Behaviors(resp)) => Ok(resp),
846            Some(_) => Err(ClientError::UnexpectedSubsystem("behaviors")),
847            None => Err(ClientError::MissingSubsystem),
848        }
849    }
850
851    fn call_keymap(
852        &mut self,
853        request_type: zmk::keymap::request::RequestType,
854    ) -> Result<zmk::keymap::Response, ClientError> {
855        let request = zmk::keymap::Request {
856            request_type: Some(request_type),
857        };
858        let rr = self.call(studio::request::Subsystem::Keymap(request))?;
859
860        match rr.subsystem {
861            Some(studio::request_response::Subsystem::Keymap(resp)) => Ok(resp),
862            Some(_) => Err(ClientError::UnexpectedSubsystem("keymap")),
863            None => Err(ClientError::MissingSubsystem),
864        }
865    }
866
867    fn call(
868        &mut self,
869        subsystem: studio::request::Subsystem,
870    ) -> Result<studio::RequestResponse, ClientError> {
871        let request_id = self.next_request_id;
872        self.next_request_id = self.next_request_id.wrapping_add(1);
873
874        let request = studio::Request {
875            request_id,
876            subsystem: Some(subsystem),
877        };
878        let bytes = encode_request(&request);
879        self.io.write_all(&bytes)?;
880
881        loop {
882            let response = self.read_next_response()?;
883            match response.r#type {
884                Some(studio::response::Type::Notification(notification)) => {
885                    self.notifications.push_back(notification);
886                }
887                Some(studio::response::Type::RequestResponse(rr)) => {
888                    if rr.request_id != request_id {
889                        return Err(ClientError::UnexpectedRequestId {
890                            expected: request_id,
891                            actual: rr.request_id,
892                        });
893                    }
894
895                    if let Some(studio::request_response::Subsystem::Meta(meta)) = &rr.subsystem {
896                        match meta.response_type {
897                            Some(zmk::meta::response::ResponseType::NoResponse(true)) => {
898                                return Err(ClientError::NoResponse);
899                            }
900                            Some(zmk::meta::response::ResponseType::SimpleError(raw)) => {
901                                let cond =
902                                    zmk::meta::ErrorConditions::try_from(raw).map_err(|_| {
903                                        ClientError::UnknownEnumValue {
904                                            field: "meta.simple_error",
905                                            value: raw,
906                                        }
907                                    })?;
908                                return Err(ClientError::Meta(cond));
909                            }
910                            _ => return Err(ClientError::MissingResponseType),
911                        }
912                    }
913
914                    return Ok(rr);
915                }
916                None => return Err(ClientError::MissingResponseType),
917            }
918        }
919    }
920
921    fn read_next_response(&mut self) -> Result<studio::Response, ClientError> {
922        if let Some(response) = self.responses.pop_front() {
923            return Ok(response);
924        }
925
926        loop {
927            let read = self.io.read(&mut self.read_buffer)?;
928            if read == 0 {
929                return Err(ClientError::Io(std::io::Error::new(
930                    std::io::ErrorKind::UnexpectedEof,
931                    "Transport reached EOF",
932                )));
933            }
934
935            let decoded = decode_responses(&mut self.decoder, &self.read_buffer[..read])?;
936            self.responses.extend(decoded);
937
938            if let Some(response) = self.responses.pop_front() {
939                return Ok(response);
940            }
941        }
942    }
943}
944
945fn binding_at(
946    keymap: &zmk::keymap::Keymap,
947    layer_id: u32,
948    key_position: i32,
949) -> Option<zmk::keymap::BehaviorBinding> {
950    let pos = usize::try_from(key_position).ok()?;
951    let layer = keymap.layers.iter().find(|l| l.id == layer_id)?;
952    layer.bindings.get(pos).copied()
953}
954
955#[cfg(feature = "serial")]
956impl StudioClient<SerialTransport> {
957    /// Convenience constructor for opening a serial transport and wrapping it in a client.
958    pub fn open_serial(path: &str) -> Result<Self, SerialTransportError> {
959        Ok(Self::new(SerialTransport::open(path)?))
960    }
961}
962
963#[cfg(feature = "ble")]
964impl StudioClient<BleTransport> {
965    /// Convenience constructor for connecting to the first matching BLE device.
966    pub fn connect_ble() -> Result<Self, BleTransportError> {
967        Ok(Self::new(BleTransport::connect_first()?))
968    }
969}