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