File Coverage

blib/lib/AnyEvent/I3.pm
Criterion Covered Total %
statement 83 176 47.1
branch 7 36 19.4
condition 2 5 40.0
subroutine 23 39 58.9
pod 12 12 100.0
total 127 268 47.3


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