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