]> git.sur5r.net Git - i3/i3/blob - lib/AnyEvent/I3.pm
support the window event, tag 0.15
[i3/i3] / lib / AnyEvent / I3.pm
1 package AnyEvent::I3;
2 # vim:ts=4:sw=4:expandtab
3
4 use strict;
5 use warnings;
6 use JSON::XS;
7 use AnyEvent::Handle;
8 use AnyEvent::Socket;
9 use AnyEvent;
10 use Encode;
11 use Scalar::Util qw(tainted);
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<AnyEvent::I3> object and returns it.
75
76 C<path> 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<AnyEvent::I3> 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 use Exporter qw(import);
86 use base 'Exporter';
87
88 our @EXPORT = qw(i3);
89
90 use constant TYPE_COMMAND => 0;
91 use constant TYPE_GET_WORKSPACES => 1;
92 use constant TYPE_SUBSCRIBE => 2;
93 use constant TYPE_GET_OUTPUTS => 3;
94 use constant TYPE_GET_TREE => 4;
95 use constant TYPE_GET_MARKS => 5;
96 use constant TYPE_GET_BAR_CONFIG => 6;
97 use constant TYPE_GET_VERSION => 7;
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     AnyEvent::I3->new(@_)
120 }
121
122 # Calls i3, even when running in taint mode.
123 sub _call_i3 {
124     my ($args) = @_;
125
126     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     (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     if ($path_tainted) {
136         my @dirs = split /:/, $ENV{PATH};
137         my @filtered = grep !/^\./, @dirs;
138         if (scalar @dirs != scalar @filtered) {
139             $ENV{PATH} = join ':', @filtered;
140             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     delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
148     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     if ($result =~ /^([^\0]+)$/) {
152         return $1;
153     }
154
155     warn "Calling i3 $args failed. Is DISPLAY set and is i3 in your PATH?";
156     return undef;
157 }
158
159 =head2 $i3 = AnyEvent::I3->new([ $path ])
160
161 Creates a new C<AnyEvent::I3> object and returns it.
162
163 C<path> 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<AnyEvent::I3> 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     my ($class, $path) = @_;
171
172     $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     $path ||= '~/.i3/ipc.sock';
177
178     # Check if we need to resolve ~
179     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         my $home = (getpwuid($<))[7];
183         die "Could not get home directory" unless $home and -d $home;
184         $path =~ s/~/$home/g;
185     }
186
187     bless { path => $path } => $class;
188 }
189
190 =head2 $i3->connect
191
192 Establishes the connection to i3. Returns an C<AnyEvent::CondVar> 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     my ($self) = @_;
203     my $cv = AnyEvent->condvar;
204
205     tcp_connect "unix/", $self->{path}, sub {
206         my ($fh) = @_;
207
208         return $cv->send(0) unless $fh;
209
210         $self->{ipchdl} = AnyEvent::Handle->new(
211             fh => $fh,
212             on_read => sub { my ($hdl) = @_; $self->_data_available($hdl) },
213             on_error => sub {
214                 my ($hdl, $fatal, $msg) = @_;
215                 delete $self->{ipchdl};
216                 $hdl->destroy;
217
218                 my $cb = $self->{callbacks};
219
220                 # Trigger all one-time callbacks with undef
221                 for my $type (keys %{$cb}) {
222                     next if ($type & $event_mask) == $event_mask;
223                     $cb->{$type}->();
224                     delete $cb->{$type};
225                 }
226
227                 # Trigger _error callback, if set
228                 my $type = $events{_error};
229                 return unless defined($cb->{$type});
230                 $cb->{$type}->($msg);
231             }
232         );
233
234         $cv->send(1)
235     };
236
237     $cv
238 }
239
240 sub _data_available {
241     my ($self, $hdl) = @_;
242
243     $hdl->unshift_read(
244         chunk => length($magic) + 4 + 4,
245         sub {
246             my $header = $_[1];
247             # Unpack message length and read the payload
248             my ($len, $type) = unpack("LL", substr($header, length($magic)));
249             $hdl->unshift_read(
250                 chunk => $len,
251                 sub { $self->_handle_i3_message($type, $_[1]) }
252             );
253         }
254     );
255 }
256
257 sub _handle_i3_message {
258     my ($self, $type, $payload) = @_;
259
260     return unless defined($self->{callbacks}->{$type});
261
262     my $cb = $self->{callbacks}->{$type};
263     $cb->(decode_json $payload);
264
265     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     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     my ($self, $callbacks) = @_;
302
303     # Register callbacks for each message type
304     for my $key (keys %{$callbacks}) {
305         my $type = $events{$key};
306         $self->{callbacks}->{$type} = $callbacks->{$key};
307     }
308
309     $self->message(TYPE_SUBSCRIBE, [ keys %{$callbacks} ])
310 }
311
312 =head2 $i3->message($type, $content)
313
314 Sends a message of the specified C<type> to i3, possibly containing the data
315 structure C<content> (or C<content>, encoded as utf8, if C<content> 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     my ($self, $type, $content) = @_;
326
327     die "No message type specified" unless defined($type);
328
329     die "No connection to i3" unless defined($self->{ipchdl});
330
331     my $payload = "";
332     if ($content) {
333         if (not ref($content)) {
334             # Convert from Perl’s internal encoding to UTF8 octets
335             $payload = encode_utf8($content);
336         } else {
337             $payload = encode_json $content;
338         }
339     }
340     my $message = $magic . pack("LL", length($payload), $type) . $payload;
341     $self->{ipchdl}->push_write($message);
342
343     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             my ($reply) = @_;
350             $cv->send($reply);
351             undef $self->{callbacks}->{$type};
352         };
353
354     $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     my ($self) = @_;
367
368     return if defined($self->{ipchdl});
369
370     $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     my ($self) = @_;
383
384     $self->_ensure_connection;
385
386     $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     my ($self) = @_;
399
400     $self->_ensure_connection;
401
402     $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     my ($self) = @_;
415
416     $self->_ensure_connection;
417
418     $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     my ($self) = @_;
431
432     $self->_ensure_connection;
433
434     $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     my ($self, $id) = @_;
447
448     $self->_ensure_connection;
449
450     $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     my ($self) = @_;
464
465     $self->_ensure_connection;
466
467     my $cv = AnyEvent->condvar;
468
469     my $version_cv = $self->message(TYPE_GET_VERSION);
470     my $timeout;
471     $timeout = AnyEvent->timer(
472         after => 1,
473         cb => sub {
474             warn "Falling back to i3 --version since the running i3 doesn’t support GET_VERSION yet.";
475             my $version = _call_i3('--version');
476             $version =~ s/^i3 version //;
477             my $patch = 0;
478             my ($major, $minor) = ($version =~ /^([0-9]+)\.([0-9]+)/);
479             if ($version =~ /^[0-9]+\.[0-9]+\.([0-9]+)/) {
480                 $patch = $1;
481             }
482             # Strip everything from the © sign on.
483             $version =~ s/ ©.*$//g;
484             $cv->send({
485                 major => int($major),
486                 minor => int($minor),
487                 patch => int($patch),
488                 human_readable => $version,
489             });
490             undef $timeout;
491         },
492     );
493     $version_cv->cb(sub {
494         undef $timeout;
495         $cv->send($version_cv->recv);
496     });
497
498     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     my ($self, $content) = @_;
511
512     $self->_ensure_connection;
513
514     $self->message(TYPE_COMMAND, $content)
515 }
516
517 =head1 AUTHOR
518
519 Michael Stapelberg, C<< <michael at i3wm.org> >>
520
521 =head1 BUGS
522
523 Please report any bugs or feature requests to C<bug-anyevent-i3 at
524 rt.cpan.org>, or through the web interface at
525 L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=AnyEvent-I3>.  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<http://rt.cpan.org/NoAuth/Bugs.html?Dist=AnyEvent-I3>
542
543 =item * The i3 window manager website
544
545 L<http://i3wm.org>
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