2 # vim:ts=4:sw=4:expandtab
11 use Scalar::Util qw(tainted);
16 AnyEvent::I3 - communicate with the i3 window manager
20 our $VERSION = '0.18';
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.
32 use AnyEvent::I3 qw(:all);
36 $i3->connect->recv or die "Error connecting";
37 say "Connected to i3";
39 my $workspaces = $i3->message(TYPE_GET_WORKSPACES)->recv;
40 say "Currently, you use " . @{$workspaces} . " workspaces";
42 ...or, using the sugar methods:
46 my $workspaces = i3->get_workspaces->recv;
47 say "Currently, you use " . @{$workspaces} . " workspaces";
49 A somewhat more involved example which dumps the i3 layout tree whenever there
58 $i3->connect->recv or die "Error connecting to i3";
62 $i3->get_tree->cb(sub {
64 say "tree: " . Dumper($tree);
67 })->recv->{success} or die "Error subscribing to events";
73 =head2 $i3 = i3([ $path ]);
75 Creates a new C<AnyEvent::I3> object and returns it.
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.
82 =head1 SUBROUTINES/METHODS
86 use Exporter qw(import);
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;
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)
111 our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } );
113 my $magic = "i3-ipc";
115 # TODO: auto-generate this from the header file? (i3/ipc.h)
116 my $event_mask = (1 << 31);
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,
130 AnyEvent::I3->new(@_)
133 # Calls i3, even when running in taint mode.
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} =~ /(.*)/);
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.
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}"|;
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]+)$/) {
166 warn "Calling i3 $args failed. Is DISPLAY set and is i3 in your PATH?";
170 =head2 $i3 = AnyEvent::I3->new([ $path ])
172 Creates a new C<AnyEvent::I3> object and returns it.
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.
181 my ($class, $path) = @_;
183 $path = _call_i3('--get-socketpath') unless $path;
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';
189 # Check if we need to resolve ~
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;
198 bless { path => $path } => $class;
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.
207 if ($i3->connect->recv) {
208 say "Connected to i3";
214 my $cv = AnyEvent->condvar;
216 tcp_connect "unix/", $self->{path}, sub {
219 return $cv->send(0) unless $fh;
221 $self->{ipchdl} = AnyEvent::Handle->new(
223 on_read => sub { my ($hdl) = @_; $self->_data_available($hdl) },
225 my ($hdl, $fatal, $msg) = @_;
226 delete $self->{ipchdl};
229 my $cb = $self->{callbacks};
231 # Trigger all one-time callbacks with undef
232 for my $type (keys %{$cb}) {
233 next if ($type & $event_mask) == $event_mask;
238 # Trigger _error callback, if set
239 my $type = $events{_error};
240 return unless defined($cb->{$type});
241 $cb->{$type}->($msg);
251 sub _data_available {
252 my ($self, $hdl) = @_;
255 chunk => length($magic) + 4 + 4,
258 # Unpack message length and read the payload
259 my ($len, $type) = unpack("LL", substr($header, length($magic)));
262 sub { $self->_handle_i3_message($type, $_[1]) }
268 sub _handle_i3_message {
269 my ($self, $type, $payload) = @_;
271 return unless defined($self->{callbacks}->{$type});
273 my $cb = $self->{callbacks}->{$type};
274 $cb->(decode_json $payload);
276 return if ($type & $event_mask) == $event_mask;
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};
283 =head2 $i3->subscribe(\%callbacks)
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.
289 workspace => sub { say "Workspaces changed" }
292 if ($i3->subscribe(\%callbacks)->recv->{success}) {
293 say "Successfully subscribed";
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.
303 say "I am sorry. I am so sorry: $msg";
308 $i3->subscribe(\%callbacks)->recv;
312 my ($self, $callbacks) = @_;
314 # Register callbacks for each message type
315 for my $key (keys %{$callbacks}) {
316 my $type = $events{$key};
317 $self->{callbacks}->{$type} = $callbacks->{$key};
320 $self->message(TYPE_SUBSCRIBE, [ keys %{$callbacks} ])
323 =head2 $i3->message($type, $content)
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.
329 my $reply = $i3->message(TYPE_RUN_COMMAND, "reload")->recv;
330 if ($reply->{success}) {
331 say "Configuration successfully reloaded";
336 my ($self, $type, $content) = @_;
338 confess "No message type specified" unless defined($type);
340 confess "No connection to i3" unless defined($self->{ipchdl});
344 if (not ref($content)) {
345 # Convert from Perl’s internal encoding to UTF8 octets
346 $payload = encode_utf8($content);
348 $payload = encode_json $content;
351 my $message = $magic . pack("LL", length($payload), $type) . $payload;
352 $self->{ipchdl}->push_write($message);
354 my $cv = AnyEvent->condvar;
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} =
362 undef $self->{callbacks}->{$type};
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
376 sub _ensure_connection {
379 return if defined($self->{ipchdl});
381 $self->connect->recv or confess "Unable to connect to i3 (socket path " . $self->{path} . ")";
384 =head2 get_workspaces
386 Gets the current workspaces from i3.
388 my $ws = i3->get_workspaces->recv;
395 $self->_ensure_connection;
397 $self->message(TYPE_GET_WORKSPACES)
402 Gets the current outputs from i3.
404 my $outs = i3->get_outputs->recv;
411 $self->_ensure_connection;
413 $self->message(TYPE_GET_OUTPUTS)
418 Gets the layout tree from i3 (>= v4.0).
420 my $tree = i3->get_tree->recv;
427 $self->_ensure_connection;
429 $self->message(TYPE_GET_TREE)
434 Gets all the window identifier marks from i3 (>= v4.1).
436 my $marks = i3->get_marks->recv;
443 $self->_ensure_connection;
445 $self->message(TYPE_GET_MARKS)
448 =head2 get_bar_config
450 Gets the bar configuration for the specific bar id from i3 (>= v4.1).
452 my $config = i3->get_bar_config($id)->recv;
457 my ($self, $id) = @_;
459 $self->_ensure_connection;
461 $self->message(TYPE_GET_BAR_CONFIG, $id)
466 Gets the i3 version via IPC, with a fall-back that parses the output of i3
467 --version (for i3 < v4.3).
469 my $version = i3->get_version()->recv;
470 say "major: " . $version->{major} . ", minor = " . $version->{minor};
476 $self->_ensure_connection;
478 my $cv = AnyEvent->condvar;
480 my $version_cv = $self->message(TYPE_GET_VERSION);
482 $timeout = AnyEvent->timer(
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 //;
489 my ($major, $minor) = ($version =~ /^([0-9]+)\.([0-9]+)/);
490 if ($version =~ /^[0-9]+\.[0-9]+\.([0-9]+)/) {
493 # Strip everything from the © sign on.
494 $version =~ s/ ©.*$//g;
496 major => int($major),
497 minor => int($minor),
498 patch => int($patch),
499 human_readable => $version,
504 $version_cv->cb(sub {
506 $cv->send($version_cv->recv);
514 Gets the raw last read config from i3. Requires i3 >= 4.14
520 $self->_ensure_connection;
522 $self->message(TYPE_GET_CONFIG);
527 Sends a tick event. Requires i3 >= 4.15
531 my ($self, $payload) = @_;
533 $self->_ensure_connection;
535 $self->message(TYPE_SEND_TICK, $payload);
540 Sends an i3 sync event. Requires i3 >= 4.16
544 my ($self, $payload) = @_;
546 $self->_ensure_connection;
548 $self->message(TYPE_SYNC, $payload);
551 =head2 command($content)
553 Makes i3 execute the given command
555 my $reply = i3->command("reload")->recv;
556 die "command failed" unless $reply->{success};
560 my ($self, $content) = @_;
562 $self->_ensure_connection;
564 $self->message(TYPE_RUN_COMMAND, $content)
569 Michael Stapelberg, C<< <michael at i3wm.org> >>
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
581 You can find documentation for this module with the perldoc command.
585 You can also look for information at:
589 =item * RT: CPAN's request tracker
591 L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=AnyEvent-I3>
593 =item * The i3 window manager website
600 =head1 ACKNOWLEDGEMENTS
603 =head1 LICENSE AND COPYRIGHT
605 Copyright 2010-2012 Michael Stapelberg.
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.
611 See https://dev.perl.org/licenses/ for more information.
616 1; # End of AnyEvent::I3