package AnyEvent::I3; # vim:ts=4:sw=4:expandtab use strict; use warnings; use JSON::XS; use AnyEvent::Handle; use AnyEvent::Socket; use AnyEvent; use Encode; =head1 NAME AnyEvent::I3 - communicate with the i3 window manager =cut our $VERSION = '0.01'; =head1 VERSION Version 0.01 =head1 SYNOPSIS This module connects to the i3 window manager using the UNIX socket based IPC interface it provides (if enabled in the configuration file). You can then subscribe to events or send messages and receive their replies. Note that as soon as you subscribe to some kind of event, you should B send any more messages as race conditions might occur. Instead, open another connection for that. use AnyEvent::I3 qw(:all); my $i3 = i3("/tmp/i3-ipc.sock"); $i3->connect->recv; say "Connected to i3"; my $workspaces = $i3->message(TYPE_GET_WORKSPACES)->recv; say "Currently, you use " . @{$workspaces} . " workspaces"; =head1 EXPORT =head2 $i3 = i3([ $path ]); Creates a new C object and returns it. C is the path of the UNIX socket to connect to. =head1 SUBROUTINES/METHODS =cut use Exporter qw(import); use base 'Exporter'; our @EXPORT = qw(i3); use constant TYPE_COMMAND => 0; use constant TYPE_GET_WORKSPACES => 1; use constant TYPE_SUBSCRIBE => 2; use constant TYPE_GET_OUTPUTS => 3; our %EXPORT_TAGS = ( 'all' => [ qw(i3 TYPE_COMMAND TYPE_GET_WORKSPACES TYPE_SUBSCRIBE TYPE_GET_OUTPUTS) ] ); our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } ); my $magic = "i3-ipc"; # TODO: auto-generate this from the header file? (i3/ipc.h) my $event_mask = (1 << 31); my %events = ( workspace => ($event_mask | 0), output => ($event_mask | 1), ); sub i3 { AnyEvent::I3->new(@_) } =head2 $i3 = AnyEvent::I3->new([ $path ]) Creates a new C object and returns it. C is the path of the UNIX socket to connect to. =cut sub new { my ($class, $path) = @_; $path ||= '/tmp/i3-ipc.sock'; bless { path => $path } => $class; } =head2 $i3->connect Establishes the connection to i3. Returns an C which will be triggered with a boolean (true if the connection was established) as soon as the connection has been established. if ($i3->connect->recv) { say "Connected to i3"; } =cut sub connect { my ($self) = @_; my $cv = AnyEvent->condvar; tcp_connect "unix/", $self->{path}, sub { my ($fh) = @_; return $cv->send(0) unless $fh; $self->{ipchdl} = AnyEvent::Handle->new( fh => $fh, on_read => sub { my ($hdl) = @_; $self->_data_available($hdl) } ); $cv->send(1) }; $cv } sub _data_available { my ($self, $hdl) = @_; $hdl->unshift_read( chunk => length($magic) + 4 + 4, sub { my $header = $_[1]; # Unpack message length and read the payload my ($len, $type) = unpack("LL", substr($header, length($magic))); $hdl->unshift_read( chunk => $len, sub { $self->_handle_i3_message($type, $_[1]) } ); } ); } sub _handle_i3_message { my ($self, $type, $payload) = @_; return unless defined($self->{callbacks}->{$type}); my $cb = $self->{callbacks}->{$type}; $cb->(decode_json $payload); } =head2 $i3->subscribe(\%callbacks) Subscribes to the given event types. This function awaits a hashref with the key being the name of the event and the value being a callback. $i3->subscribe({ workspace => sub { say "Workspaces changed" } }); =cut sub subscribe { my ($self, $callbacks) = @_; my $payload = encode_json [ keys %{$callbacks} ]; my $len = length($payload); my $message = $magic . pack("LL", $len, TYPE_SUBSCRIBE) . $payload; $self->{ipchdl}->push_write($message); # Register callbacks for each message type for my $key (keys %{$callbacks}) { my $type = $events{$key}; $self->{callbacks}->{$type} = $callbacks->{$key}; } } =head2 $i3->message($type, $content) Sends a message of the specified C to i3, possibly containing the data structure C (or C, encoded as utf8, if C is a scalar), if specified. my $reply = $i3->message(TYPE_COMMAND, "reload")->recv; if ($reply->{success}) { say "Configuration successfully reloaded"; } =cut sub message { my ($self, $type, $content) = @_; die "No message type specified" unless defined($type); my $payload = ""; if ($content) { if (not ref($content)) { # Convert from Perl’s internal encoding to UTF8 octets $payload = encode_utf8($content); } else { $payload = encode_json $content; } } my $message = $magic . pack("LL", length($payload), $type) . $payload; $self->{ipchdl}->push_write($message); my $cv = AnyEvent->condvar; # We don’t preserve the old callback as it makes no sense to # have a callback on message reply types (only on events) $self->{callbacks}->{$type} = sub { my ($reply) = @_; $cv->send($reply); undef $self->{callbacks}->{$type}; }; $cv } =head1 AUTHOR Michael Stapelberg, C<< >> =head1 BUGS Please report any bugs or feature requests to C, or through the web interface at L. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. =head1 SUPPORT You can find documentation for this module with the perldoc command. perldoc AnyEvent::I3 You can also look for information at: =over 2 =item * RT: CPAN's request tracker L =item * The i3 window manager website L =back =head1 ACKNOWLEDGEMENTS =head1 LICENSE AND COPYRIGHT Copyright 2010 Michael Stapelberg. This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License. See http://dev.perl.org/licenses/ for more information. =cut 1; # End of AnyEvent::I3