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