File Coverage

blib/lib/AnyEvent/I3.pm
Criterion Covered Total %
statement 102 209 48.8
branch 7 38 18.4
condition 2 5 40.0
subroutine 30 49 61.2
pod 15 15 100.0
total 156 316 49.3


line stmt bran cond sub pod time code
1             package AnyEvent::I3;
2             # vim:ts=4:sw=4:expandtab
3              
4 4     4   562122 use strict;
  4         6  
  4         136  
5 4     4   17 use warnings;
  4         6  
  4         185  
6 4     4   2594 use JSON::XS;
  4         24465  
  4         261  
7 4     4   2465 use AnyEvent::Handle;
  4         103313  
  4         179  
8 4     4   2747 use AnyEvent::Socket;
  4         64165  
  4         611  
9 4     4   40 use AnyEvent;
  4         5  
  4         95  
10 4     4   1715 use Encode;
  4         51772  
  4         408  
11 4     4   33 use Scalar::Util qw(tainted);
  4         21  
  4         190  
12 4     4   19 use Carp;
  4         6  
  4         314  
13              
14             =head1 NAME
15              
16             AnyEvent::I3 - communicate with the i3 window manager
17              
18             =cut
19              
20             our $VERSION = '0.19';
21              
22             =head1 VERSION
23              
24             Version 0.19
25              
26             =head1 SYNOPSIS
27              
28             This module connects to the i3 window manager using the UNIX socket based
29             IPC interface it provides (if enabled in the configuration file). You can
30             then subscribe to events or send messages and receive their replies.
31              
32             use AnyEvent::I3 qw(:all);
33              
34             my $i3 = i3();
35              
36             $i3->connect->recv or die "Error connecting";
37             say "Connected to i3";
38              
39             my $workspaces = $i3->message(TYPE_GET_WORKSPACES)->recv;
40             say "Currently, you use " . @{$workspaces} . " workspaces";
41              
42             ...or, using the sugar methods:
43              
44             use AnyEvent::I3;
45              
46             my $workspaces = i3->get_workspaces->recv;
47             say "Currently, you use " . @{$workspaces} . " workspaces";
48              
49             A somewhat more involved example which dumps the i3 layout tree whenever there
50             is a workspace event:
51              
52             use Data::Dumper;
53             use AnyEvent;
54             use AnyEvent::I3;
55              
56             my $i3 = i3();
57              
58             $i3->connect->recv or die "Error connecting to i3";
59              
60             $i3->subscribe({
61             workspace => sub {
62             $i3->get_tree->cb(sub {
63             my ($tree) = @_;
64             say "tree: " . Dumper($tree);
65             });
66             }
67             })->recv->{success} or die "Error subscribing to events";
68              
69             AE::cv->recv
70              
71             =head1 EXPORT
72              
73             =head2 $i3 = i3([ $path ]);
74              
75             Creates a new C object and returns it.
76              
77             C is an optional path of the UNIX socket to connect to. It is strongly
78             advised to NOT specify this unless you're absolutely sure you need it.
79             C will automatically figure it out by querying the running i3
80             instance on the current DISPLAY which is almost always what you want.
81              
82             =head1 SUBROUTINES/METHODS
83              
84             =cut
85              
86 4     4   62 use Exporter qw(import);
  4         7  
  4         130  
87 4     4   16 use base 'Exporter';
  4         5  
  4         465  
88              
89             our @EXPORT = qw(i3);
90              
91 4     4   23 use constant TYPE_RUN_COMMAND => 0;
  4         6  
  4         327  
92 4     4   19 use constant TYPE_COMMAND => 0;
  4         6  
  4         160  
93 4     4   29 use constant TYPE_GET_WORKSPACES => 1;
  4         6  
  4         148  
94 4     4   38 use constant TYPE_SUBSCRIBE => 2;
  4         11  
  4         154  
95 4     4   15 use constant TYPE_GET_OUTPUTS => 3;
  4         6  
  4         144  
96 4     4   22 use constant TYPE_GET_TREE => 4;
  4         5  
  4         132  
97 4     4   15 use constant TYPE_GET_MARKS => 5;
  4         5  
  4         139  
98 4     4   17 use constant TYPE_GET_BAR_CONFIG => 6;
  4         5  
  4         149  
99 4     4   17 use constant TYPE_GET_VERSION => 7;
  4         6  
  4         166  
100 4     4   18 use constant TYPE_GET_BINDING_MODES => 8;
  4         6  
  4         171  
101 4     4   17 use constant TYPE_GET_CONFIG => 9;
  4         5  
  4         143  
102 4     4   15 use constant TYPE_SEND_TICK => 10;
  4         5  
  4         216  
103 4     4   18 use constant TYPE_SYNC => 11;
  4         6  
  4         239  
104 4     4   21 use constant TYPE_GET_BINDING_STATE => 12;
  4         7  
  4         8724  
105              
106             our %EXPORT_TAGS = ( 'all' => [
107             qw(i3 TYPE_RUN_COMMAND TYPE_COMMAND TYPE_GET_WORKSPACES TYPE_SUBSCRIBE TYPE_GET_OUTPUTS
108             TYPE_GET_TREE TYPE_GET_MARKS TYPE_GET_BAR_CONFIG TYPE_GET_VERSION
109             TYPE_GET_BINDING_MODES TYPE_GET_CONFIG TYPE_SEND_TICK TYPE_SYNC
110             TYPE_GET_BINDING_STATE)
111             ] );
112              
113             our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } );
114              
115             my $magic = "i3-ipc";
116              
117             # TODO: auto-generate this from the header file? (i3/ipc.h)
118             my $event_mask = (1 << 31);
119             my %events = (
120             workspace => ($event_mask | 0),
121             output => ($event_mask | 1),
122             mode => ($event_mask | 2),
123             window => ($event_mask | 3),
124             barconfig_update => ($event_mask | 4),
125             binding => ($event_mask | 5),
126             shutdown => ($event_mask | 6),
127             tick => ($event_mask | 7),
128             _error => 0xFFFFFFFF,
129             );
130              
131             sub i3 {
132 2     2 1 348222 AnyEvent::I3->new(@_)
133             }
134              
135             # Calls i3, even when running in taint mode.
136             sub _call_i3 {
137 2     2   5 my ($args) = @_;
138              
139 2         32 my $path_tainted = tainted($ENV{PATH});
140             # This effectively circumvents taint mode checking for $ENV{PATH}. We
141             # do this because users might specify PATH explicitly to call i3 in a
142             # custom location (think ~/.bin/).
143 2         184 (local $ENV{PATH}) = ($ENV{PATH} =~ /(.*)/);
144              
145             # In taint mode, we also need to remove all relative directories from
146             # PATH (like . or ../bin). We only do this in taint mode and warn the
147             # user, since this might break a real-world use case for some people.
148 2 50       13 if ($path_tainted) {
149 2         14 my @dirs = split /:/, $ENV{PATH};
150 2         17 my @filtered = grep !/^\./, @dirs;
151 2 50       11 if (scalar @dirs != scalar @filtered) {
152 0         0 $ENV{PATH} = join ':', @filtered;
153 0         0 warn qq|Removed relative directories from PATH because you | .
154             qq|are running Perl with taint mode enabled. Remove -T | .
155             qq|to be able to use relative directories in PATH. | .
156             qq|New PATH is "$ENV{PATH}"|;
157             }
158             }
159             # Otherwise the qx() operator wont work:
160 2         27 delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
161 2         10982 chomp(my $result = qx(i3 $args));
162             # Circumventing taint mode again: the socket can be anywhere on the
163             # system and that’s okay.
164 2 50       135 if ($result =~ /^([^\0]+)$/) {
165 0         0 return $1;
166             }
167              
168 2         44 warn "Calling i3 $args failed. Is DISPLAY set and is i3 in your PATH?";
169 2         342 return undef;
170             }
171              
172             =head2 $i3 = AnyEvent::I3->new([ $path ])
173              
174             Creates a new C object and returns it.
175              
176             C is an optional path of the UNIX socket to connect to. It is strongly
177             advised to NOT specify this unless you're absolutely sure you need it.
178             C will automatically figure it out by querying the running i3
179             instance on the current DISPLAY which is almost always what you want.
180              
181             =cut
182             sub new {
183 2     2 1 21 my ($class, $path) = @_;
184              
185 2 50       23 $path = _call_i3('--get-socketpath') unless $path;
186              
187             # This is the old default path (v3.*). This fallback line can be removed in
188             # a year from now. -- Michael, 2012-07-09
189 2   50     61 $path ||= '~/.i3/ipc.sock';
190              
191             # Check if we need to resolve ~
192 2 50       43 if ($path =~ /~/) {
193             # We use getpwuid() instead of $ENV{HOME} because the latter is tainted
194             # and thus produces warnings when running tests with perl -T
195 2         975 my $home = (getpwuid($<))[7];
196 2 50 33     98 confess "Could not get home directory" unless $home and -d $home;
197 2         29 $path =~ s/~/$home/g;
198             }
199              
200 2         67 bless { path => $path } => $class;
201             }
202              
203             =head2 $i3->connect
204              
205             Establishes the connection to i3. Returns an C which will
206             be triggered with a boolean (true if the connection was established) as soon as
207             the connection has been established.
208              
209             if ($i3->connect->recv) {
210             say "Connected to i3";
211             }
212              
213             =cut
214             sub connect {
215 2     2 1 15165 my ($self) = @_;
216 2         86 my $cv = AnyEvent->condvar;
217              
218             tcp_connect "unix/", $self->{path}, sub {
219 2     2   230 my ($fh) = @_;
220              
221 2 50       32 return $cv->send(0) unless $fh;
222              
223             $self->{ipchdl} = AnyEvent::Handle->new(
224             fh => $fh,
225 0         0 on_read => sub { my ($hdl) = @_; $self->_data_available($hdl) },
  0         0  
226             on_error => sub {
227 0         0 my ($hdl, $fatal, $msg) = @_;
228 0         0 delete $self->{ipchdl};
229 0         0 $hdl->destroy;
230              
231 0         0 my $cb = $self->{callbacks};
232              
233             # Trigger all one-time callbacks with undef
234 0         0 for my $type (keys %{$cb}) {
  0         0  
235 0 0       0 next if ($type & $event_mask) == $event_mask;
236 0         0 $cb->{$type}->();
237 0         0 delete $cb->{$type};
238             }
239              
240             # Trigger _error callback, if set
241 0         0 my $type = $events{_error};
242 0 0       0 return unless defined($cb->{$type});
243 0         0 $cb->{$type}->($msg);
244             }
245 0         0 );
246              
247 0         0 $cv->send(1)
248 2         100 };
249              
250 2         843 $cv
251             }
252              
253             sub _data_available {
254 0     0     my ($self, $hdl) = @_;
255              
256             $hdl->unshift_read(
257             chunk => length($magic) + 4 + 4,
258             sub {
259 0     0     my $header = $_[1];
260             # Unpack message length and read the payload
261 0           my ($len, $type) = unpack("LL", substr($header, length($magic)));
262             $hdl->unshift_read(
263             chunk => $len,
264 0           sub { $self->_handle_i3_message($type, $_[1]) }
265 0           );
266             }
267 0           );
268             }
269              
270             sub _handle_i3_message {
271 0     0     my ($self, $type, $payload) = @_;
272              
273 0 0         return unless defined($self->{callbacks}->{$type});
274              
275 0           my $cb = $self->{callbacks}->{$type};
276 0           $cb->(decode_json $payload);
277              
278 0 0         return if ($type & $event_mask) == $event_mask;
279              
280             # If this was a one-time callback, we delete it
281             # (when connection is lost, all one-time callbacks get triggered)
282 0           delete $self->{callbacks}->{$type};
283             }
284              
285             =head2 $i3->subscribe(\%callbacks)
286              
287             Subscribes to the given event types. This function awaits a hashref with the
288             key being the name of the event and the value being a callback.
289              
290             my %callbacks = (
291             workspace => sub { say "Workspaces changed" }
292             );
293              
294             if ($i3->subscribe(\%callbacks)->recv->{success}) {
295             say "Successfully subscribed";
296             }
297              
298             The special callback with name C<_error> is called when the connection to i3
299             is killed (because of a crash, exit or restart of i3 most likely). You can
300             use it to print an appropriate message and exit cleanly or to try to reconnect.
301              
302             my %callbacks = (
303             _error => sub {
304             my ($msg) = @_;
305             say "I am sorry. I am so sorry: $msg";
306             exit 1;
307             }
308             );
309              
310             $i3->subscribe(\%callbacks)->recv;
311              
312             =cut
313             sub subscribe {
314 0     0 1   my ($self, $callbacks) = @_;
315              
316             # Register callbacks for each message type
317 0           for my $key (keys %{$callbacks}) {
  0            
318 0 0         if (!exists $events{$key}) {
319 0           warn "Could not subscribe to event type '$key'." .
320             " Supported events are " . join(" ", sort keys %events), $/;
321 0           next;
322             }
323 0           my $type = $events{$key};
324 0           $self->{callbacks}->{$type} = $callbacks->{$key};
325             }
326              
327 0           $self->message(TYPE_SUBSCRIBE, [ keys %{$callbacks} ])
  0            
328             }
329              
330             =head2 $i3->message($type, $content)
331              
332             Sends a message of the specified C to i3, possibly containing the data
333             structure C (or C, encoded as utf8, if C is a
334             scalar), if specified.
335              
336             my $reply = $i3->message(TYPE_RUN_COMMAND, "reload")->recv;
337             if ($reply->{success}) {
338             say "Configuration successfully reloaded";
339             }
340              
341             =cut
342             sub message {
343 0     0 1   my ($self, $type, $content) = @_;
344              
345 0 0         confess "No message type specified" unless defined($type);
346              
347 0 0         confess "No connection to i3" unless defined($self->{ipchdl});
348              
349 0           my $payload = "";
350 0 0         if ($content) {
351 0 0         if (not ref($content)) {
352             # Convert from Perl’s internal encoding to UTF8 octets
353 0           $payload = encode_utf8($content);
354             } else {
355 0           $payload = encode_json $content;
356             }
357             }
358 0           my $message = $magic . pack("LL", length($payload), $type) . $payload;
359 0           $self->{ipchdl}->push_write($message);
360              
361 0           my $cv = AnyEvent->condvar;
362              
363             # We don’t preserve the old callback as it makes no sense to
364             # have a callback on message reply types (only on events)
365             $self->{callbacks}->{$type} =
366             sub {
367 0     0     my ($reply) = @_;
368 0           $cv->send($reply);
369 0           undef $self->{callbacks}->{$type};
370 0           };
371              
372 0           $cv
373             }
374              
375             =head1 SUGAR METHODS
376              
377             These methods intend to make your scripts as beautiful as possible. All of
378             them automatically establish a connection to i3 blockingly (if it does not
379             already exist).
380              
381             =cut
382              
383             sub _ensure_connection {
384 0     0     my ($self) = @_;
385              
386 0 0         return if defined($self->{ipchdl});
387              
388 0 0         $self->connect->recv or confess "Unable to connect to i3 (socket path " . $self->{path} . ")";
389             }
390              
391             =head2 get_workspaces
392              
393             Gets the current workspaces from i3.
394              
395             my $ws = i3->get_workspaces->recv;
396             say Dumper($ws);
397              
398             =cut
399             sub get_workspaces {
400 0     0 1   my ($self) = @_;
401              
402 0           $self->_ensure_connection;
403              
404 0           $self->message(TYPE_GET_WORKSPACES)
405             }
406              
407             =head2 get_outputs
408              
409             Gets the current outputs from i3.
410              
411             my $outs = i3->get_outputs->recv;
412             say Dumper($outs);
413              
414             =cut
415             sub get_outputs {
416 0     0 1   my ($self) = @_;
417              
418 0           $self->_ensure_connection;
419              
420 0           $self->message(TYPE_GET_OUTPUTS)
421             }
422              
423             =head2 get_tree
424              
425             Gets the layout tree from i3 (>= v4.0).
426              
427             my $tree = i3->get_tree->recv;
428             say Dumper($tree);
429              
430             =cut
431             sub get_tree {
432 0     0 1   my ($self) = @_;
433              
434 0           $self->_ensure_connection;
435              
436 0           $self->message(TYPE_GET_TREE)
437             }
438              
439             =head2 get_marks
440              
441             Gets all the window identifier marks from i3 (>= v4.1).
442              
443             my $marks = i3->get_marks->recv;
444             say Dumper($marks);
445              
446             =cut
447             sub get_marks {
448 0     0 1   my ($self) = @_;
449              
450 0           $self->_ensure_connection;
451              
452 0           $self->message(TYPE_GET_MARKS)
453             }
454              
455             =head2 get_bar_config
456              
457             Gets the bar configuration for the specific bar id from i3 (>= v4.1).
458              
459             my $config = i3->get_bar_config($id)->recv;
460             say Dumper($config);
461              
462             =cut
463             sub get_bar_config {
464 0     0 1   my ($self, $id) = @_;
465              
466 0           $self->_ensure_connection;
467              
468 0           $self->message(TYPE_GET_BAR_CONFIG, $id)
469             }
470              
471             =head2 get_version
472              
473             Gets the i3 version via IPC, with a fall-back that parses the output of i3
474             --version (for i3 < v4.3).
475              
476             my $version = i3->get_version()->recv;
477             say "major: " . $version->{major} . ", minor = " . $version->{minor};
478              
479             =cut
480             sub get_version {
481 0     0 1   my ($self) = @_;
482              
483 0           $self->_ensure_connection;
484              
485 0           my $cv = AnyEvent->condvar;
486              
487 0           my $version_cv = $self->message(TYPE_GET_VERSION);
488 0           my $timeout;
489             $timeout = AnyEvent->timer(
490             after => 1,
491             cb => sub {
492 0     0     warn "Falling back to i3 --version since the running i3 doesn’t support GET_VERSION yet.";
493 0           my $version = _call_i3('--version');
494 0           $version =~ s/^i3 version //;
495 0           my $patch = 0;
496 0           my ($major, $minor) = ($version =~ /^([0-9]+)\.([0-9]+)/);
497 0 0         if ($version =~ /^[0-9]+\.[0-9]+\.([0-9]+)/) {
498 0           $patch = $1;
499             }
500             # Strip everything from the © sign on.
501 0           $version =~ s/ ©.*$//g;
502 0           $cv->send({
503             major => int($major),
504             minor => int($minor),
505             patch => int($patch),
506             human_readable => $version,
507             });
508 0           undef $timeout;
509             },
510 0           );
511             $version_cv->cb(sub {
512 0     0     undef $timeout;
513 0           $cv->send($version_cv->recv);
514 0           });
515              
516 0           return $cv;
517             }
518              
519             =head2 get_config
520              
521             Gets the raw last read config from i3. Requires i3 >= 4.14
522              
523             =cut
524             sub get_config {
525 0     0 1   my ($self) = @_;
526              
527 0           $self->_ensure_connection;
528              
529 0           $self->message(TYPE_GET_CONFIG);
530             }
531              
532             =head2 send_tick
533              
534             Sends a tick event. Requires i3 >= 4.15
535              
536             =cut
537             sub send_tick {
538 0     0 1   my ($self, $payload) = @_;
539              
540 0           $self->_ensure_connection;
541              
542 0           $self->message(TYPE_SEND_TICK, $payload);
543             }
544              
545             =head2 sync
546              
547             Sends an i3 sync event. Requires i3 >= 4.16
548              
549             =cut
550             sub sync {
551 0     0 1   my ($self, $payload) = @_;
552              
553 0           $self->_ensure_connection;
554              
555 0           $self->message(TYPE_SYNC, $payload);
556             }
557              
558             =head2 command($content)
559              
560             Makes i3 execute the given command
561              
562             my $reply = i3->command("reload")->recv;
563             die "command failed" unless $reply->{success};
564              
565             =cut
566             sub command {
567 0     0 1   my ($self, $content) = @_;
568              
569 0           $self->_ensure_connection;
570              
571 0           $self->message(TYPE_RUN_COMMAND, $content)
572             }
573              
574             =head1 AUTHOR
575              
576             Michael Stapelberg, C<< >>
577              
578             =head1 BUGS
579              
580             Please report any bugs or feature requests to C
581             rt.cpan.org>, or through the web interface at
582             L. I will be
583             notified, and then you'll automatically be notified of progress on your bug as
584             I make changes.
585              
586             =head1 SUPPORT
587              
588             You can find documentation for this module with the perldoc command.
589              
590             perldoc AnyEvent::I3
591              
592             You can also look for information at:
593              
594             =over 2
595              
596             =item * RT: CPAN's request tracker
597              
598             L
599              
600             =item * The i3 window manager website
601              
602             L
603              
604             =back
605              
606              
607             =head1 ACKNOWLEDGEMENTS
608              
609              
610             =head1 LICENSE AND COPYRIGHT
611              
612             Copyright 2010-2012 Michael Stapelberg.
613              
614             This program is free software; you can redistribute it and/or modify it
615             under the terms of either: the GNU General Public License as published
616             by the Free Software Foundation; or the Artistic License.
617              
618             See https://dev.perl.org/licenses/ for more information.
619              
620              
621             =cut
622              
623             1; # End of AnyEvent::I3