1#![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#[derive(Debug)]
44pub enum EstablishError {
45 GetSocketPathError(io::Error),
47 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#[derive(Debug)]
76pub enum MessageError {
77 Send(io::Error),
79 Receive(io::Error),
81 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 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()); bytes.write_u32::<LittleEndian>(payload.len() as u32)?; bytes.write_u32::<LittleEndian>(message_type)?; bytes.extend(payload.bytes()); self.write_all(&bytes[..])
152 }
153
154 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#[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 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 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#[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#[derive(Debug)]
260pub struct I3EventListener {
261 stream: UnixStream,
262}
263
264impl I3EventListener {
265 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 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 pub fn listen(&mut self) -> EventIterator {
303 EventIterator {
304 stream: &mut self.stream,
305 }
306 }
307}
308
309#[derive(Debug)]
311pub struct I3Connection {
312 stream: UnixStream,
313}
314
315impl I3Connection {
316 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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}