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