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