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