i3ipc_jl/
lib.rs

1//! A library for controlling i3-wm through its ipc interface.
2//!
3//! Using `I3Connection` you
4//! could send a command or get the hierarchy of containers. With
5//! `I3EventListener` you could listen for when the focused window changes. One of the goals is
6//! is to make this process as fool-proof as possible: usage should follow from the type
7//! signatures.
8//!
9//! The types in the `event` and `reply` modules are near direct translations from the JSON
10//! used to talk to i3. The relevant
11//! documentation (meaning of each json object and field) is shamelessly stolen from the
12//! [site](https://i3wm.org/docs/ipc.html)
13//! and put into those modules.
14//!
15//! This library should cover all of i3's documented ipc features. If it's missing something
16//! please open an issue on github.
17
18#![cfg_attr(feature = "dox", feature(doc_cfg))]
19
20extern crate byteorder;
21#[macro_use]
22extern crate log;
23extern crate serde;
24extern crate serde_json;
25
26use std::error::Error;
27use std::io::prelude::*;
28use std::os::unix::net::UnixStream;
29use std::str::FromStr;
30use std::{env, fmt, io, process};
31
32use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
33use serde_json as json;
34
35mod common;
36pub mod event;
37pub mod reply;
38
39/// An error initializing a connection.
40///
41/// It first involves first getting the i3 socket path, then connecting to the socket. Either part
42/// could go wrong which is why there are two possibilities here.
43#[derive(Debug)]
44pub enum EstablishError {
45    /// An error while getting the socket path
46    GetSocketPathError(io::Error),
47    /// An error while accessing the socket
48    SocketError(io::Error),
49}
50
51impl Error for EstablishError {
52    fn cause(&self) -> Option<&dyn Error> {
53        match *self {
54            EstablishError::GetSocketPathError(ref e) | EstablishError::SocketError(ref e) => {
55                Some(e)
56            }
57        }
58    }
59}
60
61impl fmt::Display for EstablishError {
62    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
63        match *self {
64            EstablishError::GetSocketPathError(_) => {
65                write!(f, "Couldn't determine i3's socket path")
66            }
67            EstablishError::SocketError(_) => {
68                write!(f, "Found i3's socket path but failed to connect")
69            }
70        }
71    }
72}
73
74/// An error sending or receiving a message.
75#[derive(Debug)]
76pub enum MessageError {
77    /// Network error sending the message.
78    Send(io::Error),
79    /// Network error receiving the response.
80    Receive(io::Error),
81    /// Got the response but couldn't parse the JSON.
82    JsonCouldntParse(json::Error),
83}
84
85impl Error for MessageError {
86    fn cause(&self) -> Option<&dyn Error> {
87        match *self {
88            MessageError::Send(ref e) | MessageError::Receive(ref e) => Some(e),
89            MessageError::JsonCouldntParse(ref e) => Some(e),
90        }
91    }
92}
93
94impl fmt::Display for MessageError {
95    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
96        match *self {
97            MessageError::Send(_) => write!(f, "Network error while sending message to i3"),
98            MessageError::Receive(_) => write!(f, "Network error while receiving message from i3"),
99            MessageError::JsonCouldntParse(_) => {
100                write!(f, "Got a response from i3 but couldn't parse the JSON")
101            }
102        }
103    }
104}
105
106fn get_socket_path() -> io::Result<String> {
107    if let Ok(sockpath) = env::var("I3SOCK") {
108        return Ok(sockpath);
109    }
110    // Sway support is an untested and unsupported feature
111    if let Ok(sockpath) = env::var("SWAYSOCK") {
112        return Ok(sockpath);
113    }
114
115    let output = process::Command::new("i3")
116        .arg("--get-socketpath")
117        .output()?;
118    if output.status.success() {
119        Ok(String::from_utf8_lossy(&output.stdout)
120            .trim_end_matches('\n')
121            .to_owned())
122    } else {
123        let prefix = "i3 --get-socketpath didn't return 0";
124        let error_text = if !output.stderr.is_empty() {
125            format!("{}. stderr: {:?}", prefix, output.stderr)
126        } else {
127            prefix.to_owned()
128        };
129        let error = io::Error::new(io::ErrorKind::Other, error_text);
130        Err(error)
131    }
132}
133
134trait I3Funcs {
135    fn send_i3_message(&mut self, message_type: u32, payload: &str) -> io::Result<()>;
136    fn receive_i3_message(&mut self) -> io::Result<(u32, String)>;
137    fn send_receive_i3_message<T: serde::de::DeserializeOwned>(
138        &mut self,
139        message_type: u32,
140        payload: &str,
141    ) -> Result<T, MessageError>;
142}
143
144impl I3Funcs for UnixStream {
145    fn send_i3_message(&mut self, message_type: u32, payload: &str) -> io::Result<()> {
146        let mut bytes = Vec::with_capacity(14 + payload.len());
147        bytes.extend("i3-ipc".bytes()); // 6 bytes
148        bytes.write_u32::<LittleEndian>(payload.len() as u32)?; // 4 bytes
149        bytes.write_u32::<LittleEndian>(message_type)?; // 4 bytes
150        bytes.extend(payload.bytes()); // payload.len() bytes
151        self.write_all(&bytes[..])
152    }
153
154    /// returns a tuple of (message type, payload)
155    fn receive_i3_message(&mut self) -> io::Result<(u32, String)> {
156        let mut magic_data = [0_u8; 6];
157        self.read_exact(&mut magic_data)?;
158        let magic_string = String::from_utf8_lossy(&magic_data);
159        if magic_string != "i3-ipc" {
160            let error_text = format!(
161                "unexpected magic string: expected 'i3-ipc' but got {}",
162                magic_string
163            );
164            return Err(io::Error::new(io::ErrorKind::Other, error_text));
165        }
166        let payload_len = self.read_u32::<LittleEndian>()?;
167        let message_type = self.read_u32::<LittleEndian>()?;
168        let mut payload_data = vec![0_u8; payload_len as usize];
169        self.read_exact(&mut payload_data[..])?;
170        let payload_string = String::from_utf8_lossy(&payload_data).into_owned();
171        Ok((message_type, payload_string))
172    }
173
174    fn send_receive_i3_message<T: serde::de::DeserializeOwned>(
175        &mut self,
176        message_type: u32,
177        payload: &str,
178    ) -> Result<T, MessageError> {
179        if let Err(e) = self.send_i3_message(message_type, payload) {
180            return Err(MessageError::Send(e));
181        }
182        let received = match self.receive_i3_message() {
183            Ok((received_type, payload)) => {
184                assert_eq!(message_type, received_type);
185                payload
186            }
187            Err(e) => {
188                return Err(MessageError::Receive(e));
189            }
190        };
191        match json::from_str(&received) {
192            Ok(v) => Ok(v),
193            Err(e) => Err(MessageError::JsonCouldntParse(e)),
194        }
195    }
196}
197
198/// Iterates over events from i3.
199///
200/// Each element may be `Err` or `Ok` (Err for an issue with the socket connection or data sent
201/// from i3).
202#[derive(Debug)]
203pub struct EventIterator<'a> {
204    stream: &'a mut UnixStream,
205}
206
207impl<'a> Iterator for EventIterator<'a> {
208    type Item = Result<event::Event, MessageError>;
209
210    fn next(&mut self) -> Option<Self::Item> {
211        /// the msgtype passed in should have its highest order bit stripped
212        /// makes the i3 event
213        fn build_event(msgtype: u32, payload: &str) -> Result<event::Event, json::Error> {
214            Ok(match msgtype {
215                0 => event::Event::WorkspaceEvent(event::WorkspaceEventInfo::from_str(payload)?),
216                1 => event::Event::OutputEvent(event::OutputEventInfo::from_str(payload)?),
217                2 => event::Event::ModeEvent(event::ModeEventInfo::from_str(payload)?),
218                3 => event::Event::WindowEvent(event::WindowEventInfo::from_str(payload)?),
219                4 => event::Event::BarConfigEvent(event::BarConfigEventInfo::from_str(payload)?),
220                5 => event::Event::BindingEvent(event::BindingEventInfo::from_str(payload)?),
221
222                #[cfg(feature = "i3-4-14")]
223                6 => event::Event::ShutdownEvent(event::ShutdownEventInfo::from_str(payload)?),
224
225                _ => unreachable!("received an event we aren't subscribed to!"),
226            })
227        }
228
229        match self.stream.receive_i3_message() {
230            Ok((msgint, payload)) => {
231                // strip the highest order bit indicating it's an event.
232                let msgtype = (msgint << 1) >> 1;
233
234                Some(match build_event(msgtype, &payload) {
235                    Ok(event) => Ok(event),
236                    Err(e) => Err(MessageError::JsonCouldntParse(e)),
237                })
238            }
239            Err(e) => Some(Err(MessageError::Receive(e))),
240        }
241    }
242}
243
244/// A subscription for `I3EventListener`
245#[derive(Debug)]
246pub enum Subscription {
247    Workspace,
248    Output,
249    Mode,
250    Window,
251    BarConfig,
252    Binding,
253    #[cfg(feature = "i3-4-14")]
254    #[cfg_attr(feature = "dox", doc(cfg(feature = "i3-4-14")))]
255    Shutdown,
256}
257
258/// Abstraction over an ipc socket to i3. Handles events.
259#[derive(Debug)]
260pub struct I3EventListener {
261    stream: UnixStream,
262}
263
264impl I3EventListener {
265    /// Establishes the IPC connection.
266    pub fn connect() -> Result<I3EventListener, EstablishError> {
267        match get_socket_path() {
268            Ok(path) => match UnixStream::connect(path) {
269                Ok(stream) => Ok(I3EventListener { stream }),
270                Err(error) => Err(EstablishError::SocketError(error)),
271            },
272            Err(error) => Err(EstablishError::GetSocketPathError(error)),
273        }
274    }
275
276    /// Subscribes your connection to certain events.
277    pub fn subscribe(&mut self, events: &[Subscription]) -> Result<reply::Subscribe, MessageError> {
278        let json = "[ ".to_owned()
279            + &events
280                .iter()
281                .map(|s| match *s {
282                    Subscription::Workspace => "\"workspace\"",
283                    Subscription::Output => "\"output\"",
284                    Subscription::Mode => "\"mode\"",
285                    Subscription::Window => "\"window\"",
286                    Subscription::BarConfig => "\"barconfig_update\"",
287                    Subscription::Binding => "\"binding\"",
288                    #[cfg(feature = "i3-4-14")]
289                    Subscription::Shutdown => "\"shutdown\"",
290                })
291                .collect::<Vec<_>>()
292                .join(", ")[..]
293            + " ]";
294        let j: json::Value = self.stream.send_receive_i3_message(2, &json)?;
295        let is_success = j.get("success").unwrap().as_bool().unwrap();
296        Ok(reply::Subscribe {
297            success: is_success,
298        })
299    }
300
301    /// Iterate over subscribed events forever.
302    pub fn listen(&mut self) -> EventIterator {
303        EventIterator {
304            stream: &mut self.stream,
305        }
306    }
307}
308
309/// Abstraction over an ipc socket to i3. Handles messages/replies.
310#[derive(Debug)]
311pub struct I3Connection {
312    stream: UnixStream,
313}
314
315impl I3Connection {
316    /// Establishes the IPC connection.
317    pub fn connect() -> Result<I3Connection, EstablishError> {
318        match get_socket_path() {
319            Ok(path) => match UnixStream::connect(path) {
320                Ok(stream) => Ok(I3Connection { stream }),
321                Err(error) => Err(EstablishError::SocketError(error)),
322            },
323            Err(error) => Err(EstablishError::GetSocketPathError(error)),
324        }
325    }
326
327    #[deprecated(since = "0.8.0", note = "Renamed to run_command")]
328    pub fn command(&mut self, string: &str) -> Result<reply::Command, MessageError> {
329        self.run_command(string)
330    }
331
332    /// The payload of the message is a command for i3 (like the commands you can bind to keys
333    /// in the configuration file) and will be executed directly after receiving it.
334    pub fn run_command(&mut self, string: &str) -> Result<reply::Command, MessageError> {
335        let j: json::Value = self.stream.send_receive_i3_message(0, string)?;
336        let commands = j.as_array().unwrap();
337        let vec: Vec<_> = commands
338            .iter()
339            .map(|c| reply::CommandOutcome {
340                success: c.get("success").unwrap().as_bool().unwrap(),
341                error: c.get("error").map(|val| val.as_str().unwrap().to_owned()),
342            })
343            .collect();
344
345        Ok(reply::Command { outcomes: vec })
346    }
347
348    /// Gets the current workspaces.
349    pub fn get_workspaces(&mut self) -> Result<reply::Workspaces, MessageError> {
350        let j: json::Value = self.stream.send_receive_i3_message(1, "")?;
351        let jworkspaces = j.as_array().unwrap();
352        let workspaces: Vec<_> = jworkspaces
353            .iter()
354            .map(|w| reply::Workspace {
355                num: w.get("num").unwrap().as_i64().unwrap() as i32,
356                name: w.get("name").unwrap().as_str().unwrap().to_owned(),
357                visible: w.get("visible").unwrap().as_bool().unwrap(),
358                focused: w.get("focused").unwrap().as_bool().unwrap(),
359                urgent: w.get("urgent").unwrap().as_bool().unwrap(),
360                rect: common::build_rect(w.get("rect").unwrap()),
361                output: w.get("output").unwrap().as_str().unwrap().to_owned(),
362            })
363            .collect();
364        Ok(reply::Workspaces { workspaces })
365    }
366
367    /// Gets the current outputs.
368    pub fn get_outputs(&mut self) -> Result<reply::Outputs, MessageError> {
369        let j: json::Value = self.stream.send_receive_i3_message(3, "")?;
370        let joutputs = j.as_array().unwrap();
371        let outputs: Vec<_> = joutputs
372            .iter()
373            .map(|o| reply::Output {
374                name: o.get("name").unwrap().as_str().unwrap().to_owned(),
375                #[cfg(feature = "sway-1-1")]
376                make: o.get("make").unwrap().as_str().unwrap().to_owned(),
377                #[cfg(feature = "sway-1-1")]
378                model: o.get("model").unwrap().as_str().unwrap().to_owned(),
379                #[cfg(feature = "sway-1-1")]
380                serial: o.get("serial").unwrap().as_str().unwrap().to_owned(),
381                #[cfg(feature = "sway-1-1")]
382                scale: o.get("scale").map(|s| s.as_f64().unwrap().to_owned()),
383                #[cfg(feature = "sway-1-1")]
384                subpixel_hinting: o
385                    .get("subpixel_hinting")
386                    .map(|s| s.as_str().unwrap().to_owned()),
387                #[cfg(feature = "sway-1-1")]
388                transform: o.get("transform").map(|s| s.as_str().unwrap().to_owned()),
389                #[cfg(feature = "sway-1-1")]
390                modes: common::build_modes(o.get("modes").unwrap()),
391                #[cfg(feature = "sway-1-1")]
392                current_mode: o.get("current_mode").map(|s| common::build_mode(s)),
393                active: o.get("active").unwrap().as_bool().unwrap(),
394                primary: o.get("primary").unwrap().as_bool().unwrap(),
395                current_workspace: match o.get("current_workspace").unwrap().clone() {
396                    json::Value::String(c_w) => Some(c_w),
397                    json::Value::Null => None,
398                    _ => unreachable!(),
399                },
400                #[cfg(feature = "sway-1-1")]
401                dpms: o.get("dpms").unwrap().as_bool().unwrap(),
402                rect: common::build_rect(o.get("rect").unwrap()),
403            })
404            .collect();
405        Ok(reply::Outputs { outputs })
406    }
407
408    /// Gets the layout tree. i3 uses a tree as data structure which includes every container.
409    pub fn get_tree(&mut self) -> Result<reply::Node, MessageError> {
410        let val: json::Value = self.stream.send_receive_i3_message(4, "")?;
411        Ok(common::build_tree(&val))
412    }
413
414    /// Gets a list of marks (identifiers for containers to easily jump to them later).
415    pub fn get_marks(&mut self) -> Result<reply::Marks, MessageError> {
416        let marks: Vec<String> = self.stream.send_receive_i3_message(5, "")?;
417        Ok(reply::Marks { marks })
418    }
419
420    /// Gets an array with all configured bar IDs.
421    pub fn get_bar_ids(&mut self) -> Result<reply::BarIds, MessageError> {
422        let ids: Vec<String> = self.stream.send_receive_i3_message(6, "")?;
423        Ok(reply::BarIds { ids })
424    }
425
426    /// Gets the configuration of the workspace bar with the given ID.
427    pub fn get_bar_config(&mut self, id: &str) -> Result<reply::BarConfig, MessageError> {
428        let ids: json::Value = self.stream.send_receive_i3_message(6, id)?;
429        Ok(common::build_bar_config(&ids))
430    }
431
432    /// Gets the version of i3. The reply will include the major, minor, patch and human-readable
433    /// version.
434    pub fn get_version(&mut self) -> Result<reply::Version, MessageError> {
435        let j: json::Value = self.stream.send_receive_i3_message(7, "")?;
436        Ok(reply::Version {
437            major: j.get("major").unwrap().as_i64().unwrap() as i32,
438            minor: j.get("minor").unwrap().as_i64().unwrap() as i32,
439            patch: j.get("patch").unwrap().as_i64().unwrap() as i32,
440            human_readable: j
441                .get("human_readable")
442                .unwrap()
443                .as_str()
444                .unwrap()
445                .to_owned(),
446            loaded_config_file_name: j
447                .get("loaded_config_file_name")
448                .unwrap()
449                .as_str()
450                .unwrap()
451                .to_owned(),
452        })
453    }
454
455    /// Gets the list of currently configured binding modes.
456    #[cfg(feature = "i3-4-13")]
457    #[cfg_attr(feature = "dox", doc(cfg(feature = "i3-4-13")))]
458    pub fn get_binding_modes(&mut self) -> Result<reply::BindingModes, MessageError> {
459        let modes: Vec<String> = self.stream.send_receive_i3_message(8, "")?;
460        Ok(reply::BindingModes { modes })
461    }
462
463    /// Returns the last loaded i3 config.
464    #[cfg(feature = "i3-4-14")]
465    #[cfg_attr(feature = "dox", doc(cfg(feature = "i3-4-14")))]
466    pub fn get_config(&mut self) -> Result<reply::Config, MessageError> {
467        let j: json::Value = self.stream.send_receive_i3_message(9, "")?;
468        let cfg = j.get("config").unwrap().as_str().unwrap();
469        Ok(reply::Config {
470            config: cfg.to_owned(),
471        })
472    }
473}
474
475#[cfg(test)]
476mod test {
477    use event;
478    use std::str::FromStr;
479    use I3Connection;
480    use I3EventListener;
481    use Subscription;
482
483    // for the following tests send a request and get the reponse.
484    // response types are specific so often getting them at all indicates success.
485    // can't do much better without mocking an i3 installation.
486
487    #[test]
488    fn connect() {
489        I3Connection::connect().unwrap();
490    }
491
492    #[test]
493    fn run_command_nothing() {
494        let mut connection = I3Connection::connect().unwrap();
495        let result = connection.run_command("").unwrap();
496        assert_eq!(result.outcomes.len(), 0);
497    }
498
499    #[test]
500    fn run_command_single_sucess() {
501        let mut connection = I3Connection::connect().unwrap();
502        let a = connection.run_command("exec /bin/true").unwrap();
503        assert_eq!(a.outcomes.len(), 1);
504        assert!(a.outcomes[0].success);
505    }
506
507    #[test]
508    fn run_command_multiple_success() {
509        let mut connection = I3Connection::connect().unwrap();
510        let result = connection
511            .run_command("exec /bin/true; exec /bin/true")
512            .unwrap();
513        assert_eq!(result.outcomes.len(), 2);
514        assert!(result.outcomes[0].success);
515        assert!(result.outcomes[1].success);
516    }
517
518    #[test]
519    fn run_command_fail() {
520        let mut connection = I3Connection::connect().unwrap();
521        let result = connection.run_command("ThisIsClearlyNotACommand").unwrap();
522        assert_eq!(result.outcomes.len(), 1);
523        assert!(!result.outcomes[0].success);
524    }
525
526    #[test]
527    fn get_workspaces() {
528        I3Connection::connect().unwrap().get_workspaces().unwrap();
529    }
530
531    #[test]
532    fn get_outputs() {
533        I3Connection::connect().unwrap().get_outputs().unwrap();
534    }
535
536    #[test]
537    fn get_tree() {
538        I3Connection::connect().unwrap().get_tree().unwrap();
539    }
540
541    #[test]
542    fn get_marks() {
543        I3Connection::connect().unwrap().get_marks().unwrap();
544    }
545
546    #[test]
547    fn get_bar_ids() {
548        I3Connection::connect().unwrap().get_bar_ids().unwrap();
549    }
550
551    #[test]
552    fn get_bar_ids_and_one_config() {
553        let mut connection = I3Connection::connect().unwrap();
554        let ids = connection.get_bar_ids().unwrap().ids;
555        connection.get_bar_config(&ids[0]).unwrap();
556    }
557
558    #[test]
559    fn get_version() {
560        I3Connection::connect().unwrap().get_version().unwrap();
561    }
562
563    #[cfg(feature = "i3-4-13")]
564    #[test]
565    fn get_binding_modes() {
566        I3Connection::connect()
567            .unwrap()
568            .get_binding_modes()
569            .unwrap();
570    }
571
572    #[cfg(feature = "i3-4-14")]
573    #[test]
574    fn get_config() {
575        I3Connection::connect().unwrap().get_config().unwrap();
576    }
577
578    #[test]
579    fn event_subscribe() {
580        let s = I3EventListener::connect()
581            .unwrap()
582            .subscribe(&[Subscription::Workspace])
583            .unwrap();
584        assert_eq!(s.success, true);
585    }
586
587    #[test]
588    fn from_str_workspace() {
589        let json_str = r##"
590        {
591            "change": "focus",
592            "current": {
593                "id": 28489712,
594                "name": "something",
595                "type": "workspace",
596                "border": "normal",
597                "current_border_width": 2,
598                "layout": "splith",
599                "orientation": "none",
600                "percent": 30.0,
601                "rect": { "x": 1600, "y": 0, "width": 1600, "height": 1200 },
602                "window_rect": { "x": 2, "y": 0, "width": 632, "height": 366 },
603                "deco_rect": { "x": 1, "y": 1, "width": 631, "height": 365 },
604                "geometry": { "x": 6, "y": 6, "width": 10, "height": 10 },
605                "window": 1,
606                "urgent": false,
607                "focused": true
608            },
609            "old": null
610        }"##;
611        event::WorkspaceEventInfo::from_str(json_str).unwrap();
612    }
613
614    #[test]
615    fn from_str_output() {
616        let json_str = r##"{ "change": "unspecified" }"##;
617        event::OutputEventInfo::from_str(json_str).unwrap();
618    }
619
620    #[test]
621    fn from_str_mode() {
622        let json_str = r##"{ "change": "default" }"##;
623        event::ModeEventInfo::from_str(json_str).unwrap();
624    }
625
626    #[test]
627    fn from_str_window() {
628        let json_str = r##"
629        {
630            "change": "new",
631            "container": {
632                "id": 28489712,
633                "name": "something",
634                "type": "workspace",
635                "border": "normal",
636                "current_border_width": 2,
637                "layout": "splith",
638                "orientation": "none",
639                "percent": 30.0,
640                "rect": { "x": 1600, "y": 0, "width": 1600, "height": 1200 },
641                "window_rect": { "x": 2, "y": 0, "width": 632, "height": 366 },
642                "deco_rect": { "x": 1, "y": 1, "width": 631, "height": 365 },
643                "geometry": { "x": 6, "y": 6, "width": 10, "height": 10 },
644                "window": 1,
645                "window_properties": { "class": "Firefox", "instance": "Navigator", "window_role": "browser", "title": "github.com - Mozilla Firefox", "transient_for": null },
646                "urgent": false,
647                "focused": true
648            }
649        }"##;
650        event::WindowEventInfo::from_str(json_str).unwrap();
651    }
652
653    #[test]
654    fn from_str_barconfig() {
655        let json_str = r##"
656        {
657            "id": "bar-bxuqzf",
658            "mode": "dock",
659            "position": "bottom",
660            "status_command": "i3status",
661            "font": "-misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1",
662            "workspace_buttons": true,
663            "binding_mode_indicator": true,
664            "verbose": false,
665            "colors": {
666                    "background": "#c0c0c0",
667                    "statusline": "#00ff00",
668                    "focused_workspace_text": "#ffffff",
669                    "focused_workspace_bg": "#000000"
670            }
671        }"##;
672        event::BarConfigEventInfo::from_str(json_str).unwrap();
673    }
674
675    #[test]
676    fn from_str_binding_event() {
677        let json_str = r##"
678        {
679            "change": "run",
680            "binding": {
681                "command": "nop",
682                "event_state_mask": [
683                    "shift",
684                    "ctrl"
685                ],
686                "input_code": 0,
687                "symbol": "t",
688                "input_type": "keyboard"
689            }
690        }"##;
691        event::BindingEventInfo::from_str(json_str).unwrap();
692    }
693}