streamdeck_rs/lib.rs
1#[cfg(feature = "logging")]
2pub mod logging;
3pub mod registration;
4pub mod socket;
5
6pub use crate::registration::RegistrationInfo;
7pub use crate::socket::StreamDeckSocket;
8
9use serde::{de, ser};
10use serde_derive::{Deserialize, Serialize};
11use serde_repr::{Deserialize_repr, Serialize_repr};
12use std::fmt;
13
14/// A message received from the Stream Deck software.
15///
16/// - `G` represents the global settings that are persisted within the Stream Deck software.
17/// - `S` represents the settings that are persisted within the Stream Deck software.
18/// - `M` represents the messages that are received from the property inspector.
19///
20/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/)
21#[derive(Debug, Deserialize, Serialize)]
22#[serde(tag = "event", rename_all = "camelCase")]
23pub enum Message<G, S, M> {
24 /// A key has been pressed.
25 ///
26 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#keydown)
27 #[serde(rename_all = "camelCase")]
28 KeyDown {
29 /// The uuid of the action.
30 action: String,
31 /// The instance of the action (key or part of a multiaction).
32 context: String,
33 /// The device where the key was pressed.
34 device: String,
35 /// Additional information about the key press.
36 payload: KeyPayload<S>,
37 },
38 /// A key has been released.
39 ///
40 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#keyup)
41 #[serde(rename_all = "camelCase")]
42 KeyUp {
43 /// The uuid of the action.
44 action: String,
45 /// The instance of the action (key or part of a multiaction).
46 context: String,
47 /// The device where the key was pressed.
48 device: String,
49 /// Additional information about the key press.
50 payload: KeyPayload<S>,
51 },
52 /// An instance of the action has been added to the display.
53 ///
54 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#willappear)
55 #[serde(rename_all = "camelCase")]
56 WillAppear {
57 /// The uuid of the action.
58 action: String,
59 /// The instance of the action (key or part of a multiaction).
60 context: String,
61 /// The device where the action will appear, or None if it does not appear on a device.
62 device: Option<String>,
63 /// Additional information about the action's appearance.
64 payload: VisibilityPayload<S>,
65 },
66 /// An instance of the action has been removed from the display.
67 ///
68 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#willdisappear)
69 #[serde(rename_all = "camelCase")]
70 WillDisappear {
71 /// The uuid of the action.
72 action: String,
73 /// The instance of the action (key or part of a multiaction).
74 context: String,
75 /// The device where the action was visible, or None if it was not on a device.
76 device: Option<String>,
77 /// Additional information about the action's appearance.
78 payload: VisibilityPayload<S>,
79 },
80 /// The title has changed for an instance of an action.
81 ///
82 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#titleparametersdidchange)
83 #[serde(rename_all = "camelCase")]
84 TitleParametersDidChange {
85 /// The uuid of the action.
86 action: String,
87 /// The instance of the action (key or part of a multiaction).
88 context: String,
89 /// The device where the action is visible, or None if it is not on a device.
90 device: Option<String>,
91 /// Additional information about the new title.
92 payload: TitleParametersPayload<S>,
93 },
94 /// A device has connected.
95 ///
96 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#devicedidconnect)
97 #[serde(rename_all = "camelCase")]
98 DeviceDidConnect {
99 /// The ID of the device that has connected.
100 device: String,
101 /// Information about the device.
102 device_info: DeviceInfo,
103 },
104 /// A device has disconnected.
105 ///
106 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#devicediddisconnect)
107 #[serde(rename_all = "camelCase")]
108 DeviceDidDisconnect {
109 /// The ID of the device that has disconnected.
110 device: String,
111 },
112 /// An application monitored by the manifest file has launched.
113 ///
114 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#applicationdidlaunch)
115 #[serde(rename_all = "camelCase")]
116 ApplicationDidLaunch {
117 /// Information about the launched application.
118 payload: ApplicationPayload,
119 },
120 /// An application monitored by the manifest file has terminated.
121 ///
122 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#applicationdidterminate)
123 #[serde(rename_all = "camelCase")]
124 ApplicationDidTerminate {
125 /// Information about the terminated application.
126 payload: ApplicationPayload,
127 },
128 /// The property inspector has sent data.
129 ///
130 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#sendtoplugin)
131 #[serde(rename_all = "camelCase")]
132 SendToPlugin {
133 /// The uuid of the action.
134 action: String,
135 /// The instance of the action (key or part of a multiaction).
136 context: String,
137 /// Information sent from the property inspector.
138 payload: M,
139 },
140 /// The application has sent settings for an action.
141 ///
142 /// This message is sent in response to GetSettings, but also after the
143 /// property inspector changes the settings.
144 ///
145 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#didreceivesettings)
146 #[serde(rename_all = "camelCase")]
147 DidReceiveSettings {
148 /// The uuid of the action.
149 action: String,
150 /// The instance of the action (key or part of a multiaction).
151 context: String,
152 /// The device where the action exists.
153 device: String,
154 /// The current settings for the action.
155 payload: KeyPayload<S>,
156 },
157 /// The property inspector for an action has become visible.
158 ///
159 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#propertyinspectordidappear)
160 #[serde(rename_all = "camelCase")]
161 PropertyInspectorDidAppear {
162 /// The uuid of the action.
163 action: String,
164 /// The instance of the action (key or part of a multiaction).
165 context: String,
166 /// The device where the action exists.
167 device: String,
168 },
169 /// The property inspector for an action is no longer visible.
170 ///
171 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#propertyinspectordiddisappear)
172 #[serde(rename_all = "camelCase")]
173 PropertyInspectorDidDisappear {
174 /// The uuid of the action.
175 action: String,
176 /// The instance of the action (key or part of a multiaction).
177 context: String,
178 /// The device where the action exists.
179 device: String,
180 },
181 /// The application has sent settings for an action.
182 ///
183 /// This message is sent in response to GetGlobalSettings, but also after
184 /// the property inspector changes the settings.
185 ///
186 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#didreceiveglobalsettings)
187 #[serde(rename_all = "camelCase")]
188 DidReceiveGlobalSettings {
189 /// The current settings for the action.
190 payload: GlobalSettingsPayload<G>,
191 },
192 /// The computer has resumed from sleep.
193 ///
194 /// Added in Stream Deck software version 4.3.
195 ///
196 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#systemdidwakeup)
197 SystemDidWakeUp,
198 /// An event from an unsupported version of the Stream Deck software.
199 ///
200 /// This occurs when the Stream Deck software sends an event that is not
201 /// understood. Usually this will be because the Stream Deck software is
202 /// newer than the plugin, and it should be safe to ignore these.
203 #[serde(other)]
204 Unknown,
205}
206
207/// A message to be sent to the Stream Deck software.
208///
209/// - `G` represents the global settings that are persisted within the Stream Deck software.
210/// - `S` represents the action settings that are persisted within the Stream Deck software.
211/// - `M` represents the messages that are sent to the property inspector.
212///
213/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/)
214#[derive(Debug, Deserialize, Serialize)]
215#[serde(tag = "event", rename_all = "camelCase")]
216pub enum MessageOut<G, S, M> {
217 /// Set the title of an action instance.
218 ///
219 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#settitle)
220 #[serde(rename_all = "camelCase")]
221 SetTitle {
222 /// The instance of the action (key or part of a multiaction).
223 context: String,
224 /// The title to set.
225 payload: TitlePayload,
226 },
227 /// Set the image of an action instance.
228 ///
229 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#setimage)
230 #[serde(rename_all = "camelCase")]
231 SetImage {
232 /// The instance of the action (key or part of a multiaction).
233 context: String,
234 /// The image to set.
235 payload: ImagePayload,
236 },
237 /// Temporarily overlay the key image with an alert icon.
238 ///
239 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#showalert)
240 #[serde(rename_all = "camelCase")]
241 ShowAlert {
242 /// The instance of the action (key or part of a multiaction).
243 context: String,
244 },
245 /// Temporarily overlay the key image with a checkmark.
246 ///
247 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#showok)
248 #[serde(rename_all = "camelCase")]
249 ShowOk {
250 /// The instance of the action (key or part of a multiaction).
251 context: String,
252 },
253 /// Retrieve settings for an instance of an action via DidReceiveSettings.
254 ///
255 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#getsettings)
256 #[serde(rename_all = "camelCase")]
257 GetSettings {
258 /// The instance of the action (key or part of a multiaction).
259 context: String,
260 },
261 /// Store settings for an instance of an action.
262 ///
263 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#setsettings)
264 #[serde(rename_all = "camelCase")]
265 SetSettings {
266 /// The instance of the action (key or part of a multiaction).
267 context: String,
268 /// The settings to save.
269 payload: S,
270 },
271 /// Set the state of an action.
272 ///
273 /// Normally, Stream Deck changes the state of an action automatically when the key is pressed.
274 ///
275 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#setstate)
276 #[serde(rename_all = "camelCase")]
277 SetState {
278 /// The instance of the action (key or part of a multiaction).
279 context: String,
280 /// The desired state.
281 payload: StatePayload,
282 },
283 /// Send data to the property inspector.
284 ///
285 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#sendtopropertyinspector)
286 #[serde(rename_all = "camelCase")]
287 SendToPropertyInspector {
288 /// The uuid of the action.
289 action: String,
290 /// The instance of the action (key or part of a multiaction).
291 context: String,
292 /// The message to send.
293 payload: M,
294 },
295 /// Select a new profile.
296 ///
297 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#switchtoprofile)
298 #[serde(rename_all = "camelCase")]
299 SwitchToProfile {
300 /// The instance of the action (key or part of a multiaction).
301 context: String,
302 /// The device to change the profile of.
303 device: String,
304 /// The profile to activate.
305 payload: ProfilePayload,
306 },
307 /// Open a URL in the default browser.
308 ///
309 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#openurl)
310 #[serde(rename_all = "camelCase")]
311 OpenUrl {
312 /// The url to open.
313 payload: UrlPayload,
314 },
315 /// Retrieve plugin settings for via DidReceiveGlobalSettings.
316 ///
317 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#getglobalsettings)
318 #[serde(rename_all = "camelCase")]
319 GetGlobalSettings {
320 /// The instance of the action (key or part of a multiaction).
321 context: String,
322 },
323 /// Store plugin settings.
324 ///
325 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#setglobalsettings)
326 #[serde(rename_all = "camelCase")]
327 SetGlobalSettings {
328 /// The instance of the action (key or part of a multiaction).
329 context: String,
330 /// The settings to save.
331 payload: G,
332 },
333 /// Write to the log.
334 ///
335 /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#logmessage)
336 #[serde(rename_all = "camelCase")]
337 LogMessage {
338 /// The message to log.
339 payload: LogMessagePayload,
340 },
341}
342
343/// The target of a command.
344#[derive(Debug, Deserialize_repr, Serialize_repr)]
345#[repr(u8)]
346pub enum Target {
347 /// Both the device and a the display within the Stream Deck software.
348 Both = 0,
349 /// Only the device.
350 Hardware = 1,
351 /// Only the display within the Stream Deck software.
352 Software = 2,
353}
354
355/// The title to set as part of a [SetTitle](enum.MessageOut.html#variant.SetTitle) message.
356///
357/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#settitle)
358#[derive(Debug, Deserialize, Serialize)]
359#[serde(rename_all = "camelCase")]
360pub struct TitlePayload {
361 /// The new title.
362 pub title: Option<String>,
363 /// The target displays.
364 pub target: Target,
365 /// The state to set the title for. If not set, it is set for all states.
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub state: Option<u8>,
368}
369
370/// The image to set as part of a [SetImage](enum.MessageOut.html#variant.SetImage) message.
371///
372/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#setimage)
373#[derive(Debug, Deserialize, Serialize)]
374#[serde(rename_all = "camelCase")]
375pub struct ImagePayload {
376 /// An image in the form of a data URI.
377 pub image: Option<String>,
378 /// The target displays.
379 pub target: Target,
380 /// The state to set the image for. If not set, it is set for all states.
381 #[serde(skip_serializing_if = "Option::is_none")]
382 pub state: Option<u8>,
383}
384
385/// The state to set as part of a [SetState](enum.MessageOut.html#variant.SetState) message.
386///
387/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#setstate)
388#[derive(Debug, Deserialize, Serialize)]
389#[serde(rename_all = "camelCase")]
390pub struct StatePayload {
391 /// The new state.
392 pub state: u8,
393}
394
395/// The profile to activate as part of a [SwitchToProfile](enum.MessageOut.html#variant.SwitchToProfile) message.
396///
397/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#SwitchToProfile)
398#[derive(Debug, Deserialize, Serialize)]
399#[serde(rename_all = "camelCase")]
400pub struct ProfilePayload {
401 /// The name of the profile to activate.
402 pub profile: String,
403}
404
405/// The URL to launch as part of a [OpenUrl](enum.MessageOut.html#variant.OpenUrl) message.
406///
407/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#openurl)
408#[derive(Debug, Deserialize, Serialize)]
409#[serde(rename_all = "camelCase")]
410pub struct UrlPayload {
411 /// The URL to launch.
412 pub url: String,
413}
414
415/// Additional information about the key pressed.
416#[derive(Debug, Deserialize, Serialize)]
417#[serde(rename_all = "camelCase")]
418pub struct KeyPayload<S> {
419 /// The stored settings for the action instance.
420 pub settings: S,
421 /// The location of the key that was pressed, or None if this action instance is part of a multi action.
422 pub coordinates: Option<Coordinates>,
423 /// The current state of the action instance.
424 pub state: Option<u8>,
425 /// The desired state of the action instance (if this instance is part of a multi action).
426 pub user_desired_state: Option<u8>,
427 //TODO: is_in_multi_action ignored. replace coordinates with enum Location { Coordinates, MultiAction }.
428}
429
430/// Additional information about a key's appearance.
431#[derive(Debug, Deserialize, Serialize)]
432#[serde(rename_all = "camelCase")]
433pub struct VisibilityPayload<S> {
434 /// The stored settings for the action instance.
435 pub settings: S,
436 /// The location of the key, or None if this action instance is part of a multi action.
437 pub coordinates: Option<Coordinates>,
438 /// The state of the action instance.
439 pub state: Option<u8>,
440 //TODO: is_in_multi_action ignored. replace coordinates with enum Location { Coordinates, MultiAction }.
441}
442
443/// The new title of a key.
444#[derive(Debug, Deserialize, Serialize)]
445#[serde(rename_all = "camelCase")]
446pub struct TitleParametersPayload<S> {
447 /// The stored settings for the action instance.
448 pub settings: S,
449 /// The location of the key, or None if this action instance is part of a multi action.
450 pub coordinates: Coordinates,
451 /// The state of the action instance.
452 pub state: Option<u8>,
453 /// The new title.
454 pub title: String,
455 /// Additional parameters for the display of the title.
456 pub title_parameters: TitleParameters,
457}
458
459/// The new global settings.
460#[derive(Debug, Deserialize, Serialize)]
461#[serde(rename_all = "camelCase")]
462pub struct GlobalSettingsPayload<G> {
463 /// The stored settings for the plugin.
464 pub settings: G,
465}
466
467/// A log message.
468#[derive(Debug, Deserialize, Serialize)]
469#[serde(rename_all = "camelCase")]
470pub struct LogMessagePayload {
471 /// The log message text.
472 pub message: String,
473}
474
475/// Information about a hardware device.
476///
477/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#devicedidconnect)
478#[derive(Debug, Deserialize, Serialize)]
479#[serde(rename_all = "camelCase")]
480pub struct DeviceInfo {
481 /// The user-provided name of the device.
482 ///
483 /// Added in Stream Deck software version 4.3.
484 pub name: Option<String>,
485 /// The size of the device.
486 pub size: DeviceSize,
487 /// The type of the device, or None if the Stream Deck software is running with no device attached.
488 #[serde(rename = "type")]
489 pub _type: Option<DeviceType>,
490}
491
492/// Information about a monitored application that has launched or terminated.
493#[derive(Debug, Deserialize, Serialize)]
494#[serde(rename_all = "camelCase")]
495pub struct ApplicationPayload {
496 /// The name of the application.
497 pub application: String,
498}
499
500/// The location of a key on a device.
501///
502/// Locations are specified using zero-indexed values starting from the top left corner of the device.
503#[derive(Debug, Deserialize, Serialize)]
504#[serde(rename_all = "camelCase")]
505pub struct Coordinates {
506 /// The x coordinate of the key.
507 pub column: u8,
508 /// The y-coordinate of the key.
509 pub row: u8,
510}
511
512/// The vertical alignment of a title.
513///
514/// Titles are always centered horizontally.
515#[derive(Debug, Deserialize, Serialize)]
516#[serde(rename_all = "camelCase")]
517pub enum Alignment {
518 /// The title should appear at the top of the key.
519 Top,
520 /// The title should appear in the middle of the key.
521 Middle,
522 /// The title should appear at the bottom of the key.
523 Bottom,
524}
525
526/// Style information for a title.
527///
528/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#titleparametersdidchange)
529#[derive(Debug, Deserialize, Serialize)]
530#[serde(rename_all = "camelCase")]
531pub struct TitleParameters {
532 /// The name of the font family.
533 pub font_family: String,
534 /// The font size.
535 pub font_size: u8,
536 /// Whether the font is bold and/or italic.
537 pub font_style: String,
538 /// Whether the font is underlined.
539 pub font_underline: bool,
540 /// Whether the title is displayed.
541 pub show_title: bool,
542 /// The vertical alignment of the title.
543 pub title_alignment: Alignment,
544 /// The color of the title.
545 pub title_color: String,
546}
547
548/// The size of a device in keys.
549#[derive(Debug, Deserialize, Serialize)]
550#[serde(rename_all = "camelCase")]
551pub struct DeviceSize {
552 /// The number of key columns on the device.
553 pub columns: u8,
554 /// The number of key rows on the device.
555 pub rows: u8,
556}
557
558/// The type of connected hardware device.
559///
560/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/manifest/#profiles)
561#[derive(Debug)]
562pub enum DeviceType {
563 /// The [Stream Deck](https://www.elgato.com/en/gaming/stream-deck).
564 StreamDeck, // 0
565 /// The [Stream Deck Mini](https://www.elgato.com/en/gaming/stream-deck-mini).
566 StreamDeckMini, // 1
567 /// The [Stream Deck XL](https://www.elgato.com/en/gaming/stream-deck-xl).
568 ///
569 /// Added in Stream Deck software version 4.3.
570 StreamDeckXl, // 2
571 /// The [Stream Deck Mobile](https://www.elgato.com/en/gaming/stream-deck-mobile) app.
572 ///
573 /// Added in Stream Deck software version 4.3.
574 StreamDeckMobile, // 3
575 /// The G-keys in Corsair keyboards
576 ///
577 /// Added in Stream Deck software version 4.7
578 CorsairGKeys, // 4
579 /// The [Stream Deck Pedal](https://www.elgato.com/en/stream-deck-pedal).
580 ///
581 /// Added in Stream Deck software version 5.2
582 StreamDeckPedal, // 5
583 /// The [Corsair Voyager Streaming Laptop](https://www.corsair.com/us/en/voyager-a1600-gaming-streaming-pc-laptop).
584 ///
585 /// Added in Stream Deck software version 5.3
586 CorsairVoyager, // 6
587 /// The [Stream Deck +](https://www.elgato.com/en/stream-deck-plus)
588 ///
589 /// Added in Stream Deck software version 6.0
590 StreamDeckPlus, // 7
591 /// A device not documented in the 6.0 SDK.
592 Unknown(u64),
593}
594
595impl ser::Serialize for DeviceType {
596 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
597 where
598 S: ser::Serializer,
599 {
600 serializer.serialize_u64(match self {
601 DeviceType::StreamDeck => 0,
602 DeviceType::StreamDeckMini => 1,
603 DeviceType::StreamDeckXl => 2,
604 DeviceType::StreamDeckMobile => 3,
605 DeviceType::CorsairGKeys => 4,
606 DeviceType::StreamDeckPedal => 5,
607 DeviceType::CorsairVoyager => 6,
608 DeviceType::StreamDeckPlus => 7,
609 DeviceType::Unknown(value) => *value,
610 })
611 }
612}
613
614impl<'de> de::Deserialize<'de> for DeviceType {
615 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
616 where
617 D: de::Deserializer<'de>,
618 {
619 struct Visitor;
620
621 impl<'de> de::Visitor<'de> for Visitor {
622 type Value = DeviceType;
623
624 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
625 formatter.write_str("an integer")
626 }
627
628 fn visit_u64<E>(self, value: u64) -> Result<DeviceType, E>
629 where
630 E: de::Error,
631 {
632 Ok(match value {
633 0 => DeviceType::StreamDeck,
634 1 => DeviceType::StreamDeckMini,
635 2 => DeviceType::StreamDeckXl,
636 3 => DeviceType::StreamDeckMobile,
637 4 => DeviceType::CorsairGKeys,
638 5 => DeviceType::StreamDeckPedal,
639 6 => DeviceType::CorsairVoyager,
640 7 => DeviceType::StreamDeckPlus,
641 value => DeviceType::Unknown(value),
642 })
643 }
644 }
645
646 deserializer.deserialize_u64(Visitor)
647 }
648}
649
650#[derive(Clone, Debug, Eq, PartialEq)]
651pub struct Color {
652 r: u8,
653 g: u8,
654 b: u8,
655}
656
657impl ser::Serialize for Color {
658 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
659 where
660 S: ser::Serializer,
661 {
662 let html_color = format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b);
663 serializer.serialize_str(&html_color)
664 }
665}
666
667impl<'de> de::Deserialize<'de> for Color {
668 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
669 where
670 D: de::Deserializer<'de>,
671 {
672 struct Visitor;
673
674 impl<'de> de::Visitor<'de> for Visitor {
675 type Value = Color;
676
677 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
678 formatter.write_str("a hex color")
679 }
680
681 fn visit_str<E>(self, value: &str) -> Result<Color, E>
682 where
683 E: de::Error,
684 {
685 if value.len() != 7 {
686 return Err(E::invalid_length(value.len(), &self));
687 }
688
689 if &value[0..1] != "#" {
690 return Err(E::custom("expected string to begin with '#'"));
691 }
692
693 let r = u8::from_str_radix(&value[1..3], 16)
694 .map_err(|_| E::invalid_value(de::Unexpected::Str(value), &self))?;
695 let g = u8::from_str_radix(&value[3..5], 16)
696 .map_err(|_| E::invalid_value(de::Unexpected::Str(value), &self))?;
697 let b = u8::from_str_radix(&value[5..7], 16)
698 .map_err(|_| E::invalid_value(de::Unexpected::Str(value), &self))?;
699
700 Ok(Color { r, g, b })
701 }
702 }
703
704 deserializer.deserialize_str(Visitor)
705 }
706}
707
708#[cfg(test)]
709mod test {
710 use super::Color;
711
712 #[test]
713 fn color() {
714 let color_a = Color {
715 r: 0x12,
716 g: 0x34,
717 b: 0x56,
718 };
719 let color_b = Color {
720 r: 0x12,
721 g: 0x12,
722 b: 0x12,
723 };
724
725 let as_json = r##"["#123456","#121212"]"##;
726 let colors: Vec<Color> = serde_json::from_str(as_json).expect("array of colors");
727
728 assert_eq!(2, colors.len());
729 assert_eq!(color_a, colors[0]);
730 assert_eq!(color_b, colors[1]);
731
732 let json_str: String = serde_json::to_string(&vec![color_a, color_b]).expect("JSON array");
733 assert_eq!(as_json, json_str);
734 }
735}