File Coverage

blib/lib/AnyEvent/Sway.pm
Criterion Covered Total %
statement 104 206 50.4
branch 7 36 19.4
condition 2 5 40.0
subroutine 30 49 61.2
pod 15 15 100.0
total 158 311 50.8


line stmt bran cond sub pod time code
1             package AnyEvent::Sway;
2             # vim:ts=4:sw=4:expandtab
3              
4 3     3   170161 use strict;
  3         23  
  3         75  
5 3     3   12 use warnings;
  3         5  
  3         60  
6 3     3   1758 use JSON::XS;
  3         18816  
  3         141  
7 3     3   1831 use AnyEvent::Handle;
  3         67155  
  3         117  
8 3     3   1552 use AnyEvent::Socket;
  3         39075  
  3         281  
9 3     3   23 use AnyEvent;
  3         5  
  3         64  
10 3     3   1480 use Encode;
  3         26543  
  3         193  
11 3     3   20 use Scalar::Util qw(tainted);
  3         6  
  3         125  
12 3     3   15 use Carp;
  3         6  
  3         190  
13              
14             =head1 NAME
15              
16             AnyEvent::Sway - communicate with the Sway window manager
17              
18             =cut
19              
20             our $VERSION = '0.18';
21              
22             =head1 VERSION
23              
24             Version 0.18
25              
26             =head1 SYNOPSIS
27              
28             This module connects to the Sway 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.
31              
32             use AnyEvent::Sway qw(:all);
33              
34             my $sway = sway();
35              
36             $sway->connect->recv or die "Error connecting";
37             say "Connected to Sway";
38              
39             my $workspaces = $sway->message(TYPE_GET_WORKSPACES)->recv;
40             say "Currently, you use " . @{$workspaces} . " workspaces";
41              
42             ...or, using the sugar methods:
43              
44             use AnyEvent::Sway;
45              
46             my $workspaces = Sway->get_workspaces->recv;
47             say "Currently, you use " . @{$workspaces} . " workspaces";
48              
49             A somewhat more involved example which dumps the Sway layout tree whenever there
50             is a workspace event:
51              
52             use Data::Dumper;
53             use AnyEvent;
54             use AnyEvent::Sway;
55              
56             my $sway = sway();
57              
58             $sway->connect->recv or die "Error connecting to Sway";
59              
60             $sway->subscribe({
61             workspace => sub {
62             $sway->get_tree->cb(sub {
63             my ($tree) = @_;
64             say "tree: " . Dumper($tree);
65             });
66             }
67             })->recv->{success} or die "Error subscribing to events";
68              
69             AE::cv->recv
70              
71             =head1 EXPORT
72              
73             =head2 $sway = sway([ $path ]);
74              
75             Creates a new C object and returns it.
76              
77             C 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 will automatically figure it out by querying the running Sway
80             instance on the current DISPLAY which is almost always what you want.
81              
82             =head1 SUBROUTINES/METHODS
83              
84             =cut
85              
86 3     3   23 use Exporter qw(import);
  3         6  
  3         64  
87 3     3   15 use base 'Exporter';
  3         6  
  3         199  
88              
89             our @EXPORT = qw(sway);
90              
91 3     3   15 use constant TYPE_RUN_COMMAND => 0;
  3         5  
  3         131  
92 3     3   15 use constant TYPE_COMMAND => 0;
  3         3  
  3         116  
93 3     3   18 use constant TYPE_GET_WORKSPACES => 1;
  3         6  
  3         119  
94 3     3   20 use constant TYPE_SUBSCRIBE => 2;
  3         4  
  3         133  
95 3     3   15 use constant TYPE_GET_OUTPUTS => 3;
  3         5  
  3         140  
96 3     3   35 use constant TYPE_GET_TREE => 4;
  3         8  
  3         135  
97 3     3   17 use constant TYPE_GET_MARKS => 5;
  3         6  
  3         141  
98 3     3   16 use constant TYPE_GET_BAR_CONFIG => 6;
  3         4  
  3         159  
99 3     3   15 use constant TYPE_GET_VERSION => 7;
  3         5  
  3         129  
100 3     3   15 use constant TYPE_GET_BINDING_MODES => 8;
  3         6  
  3         121  
101 3     3   15 use constant TYPE_GET_CONFIG => 9;
  3         5  
  3         189  
102 3     3   19 use constant TYPE_SEND_TICK => 10;
  3         4  
  3         123  
103 3     3   16 use constant TYPE_SYNC => 11;
  3         5  
  3         134  
104 3     3   16 use constant TYPE_GET_BINDING_STATE => 12;
  3         13  
  3         5732  
105              
106             our %EXPORT_TAGS = ( 'all' => [
107             qw(sway TYPE_RUN_COMMAND TYPE_COMMAND TYPE_GET_WORKSPACES TYPE_SUBSCRIBE TYPE_GET_OUTPUTS
108             TYPE_GET_TREE TYPE_GET_MARKS TYPE_GET_BAR_CONFIG TYPE_GET_VERSION
109             TYPE_GET_BINDING_MODES TYPE_GET_CONFIG TYPE_SEND_TICK TYPE_SYNC
110             TYPE_GET_BINDING_STATE)
111             ] );
112              
113             our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } );
114              
115             my $magic = "i3-ipc";
116              
117             # TODO: auto-generate this from the header file? (Sway/ipc.h)
118             my $event_mask = (1 << 31);
119             my %events = (
120             workspace => ($event_mask | 0),
121             output => ($event_mask | 1),
122             mode => ($event_mask | 2),
123             window => ($event_mask | 3),
124             barconfig_update => ($event_mask | 4),
125             binding => ($event_mask | 5),
126             shutdown => ($event_mask | 6),
127             tick => ($event_mask | 7),
128             _error => 0xFFFFFFFF,
129             );
130              
131             sub sway
132             {
133 2     2 1 146 AnyEvent::Sway->new(@_)
134             }
135              
136             # Calls Sway, even when running in taint mode.
137             sub _call_sway
138             {
139 2     2   6 my ($args) = @_;
140              
141 2         15 my $path_tainted = tainted($ENV{PATH});
142             # This effectively circumvents taint mode checking for $ENV{PATH}. We
143             # do this because users might specify PATH explicitly to call Sway in a
144             # custom location (think ~/.bin/).
145 2         108 (local $ENV{PATH}) = ($ENV{PATH} =~ /(.*)/);
146              
147             # In taint mode, we also need to remove all relative directories from
148             # PATH (like . or ../bin). We only do this in taint mode and warn the
149             # user, since this might break a real-world use case for some people.
150 2 50       14 if ($path_tainted) {
151 2         18 my @dirs = split /:/, $ENV{PATH};
152 2         18 my @filtered = grep !/^\./, @dirs;
153 2 50       8 if (scalar @dirs != scalar @filtered) {
154 2         76 $ENV{PATH} = join ':', @filtered;
155 2         124 warn qq|Removed relative directories from PATH because you | .
156             qq|are running Perl with taint mode enabled. Remove -T | .
157             qq|to be able to use relative directories in PATH. | .
158             qq|New PATH is "$ENV{PATH}"|;
159             }
160             }
161             # Otherwise the qx() operator wont work:
162 2         29 delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
163 2         5459 chomp(my $result = qx(sway $args));
164             # Circumventing taint mode again: the socket can be anywhere on the
165             # system and that’s okay.
166 2 50       106 if ($result =~ /^([^\0]+)$/) {
167 0         0 return $1;
168             }
169              
170 2         50 warn "Calling sway $args failed. Is DISPLAY set and is sway in your PATH?";
171 2         188 return undef;
172             }
173              
174             =head2 $sway = AnyEvent::Sway->new([ $path ])
175              
176             Creates a new C object and returns it.
177              
178             C is an optional path of the UNIX socket to connect to. It is strongly
179             advised to NOT specify this unless you're absolutely sure you need it.
180             C will automatically figure it out by querying the running Sway
181             instance on the current DISPLAY which is almost always what you want.
182              
183             =cut
184             sub new
185             {
186 2     2 1 6 my ($class, $path) = @_;
187              
188 2 50       10 $path = _call_sway('--get-socketpath') unless $path;
189              
190             # This is the old default path (v3.*). This fallback line can be removed in
191             # a year from now. -- Michael, 2012-07-09
192 2   50     59 $path ||= '~/.sway/ipc.sock';
193              
194             # Check if we need to resolve ~
195 2 50       42 if ($path =~ /~/) {
196             # We use getpwuid() instead of $ENV{HOME} because the latter is tainted
197             # and thus produces warnings when running tests with perl -T
198 2         1702 my $home = (getpwuid($<))[7];
199 2 50 33     94 confess "Could not get home directory" unless $home and -d $home;
200 2         27 $path =~ s/~/$home/g;
201             }
202              
203 2         55 bless { path => $path } => $class;
204             }
205              
206             =head2 $sway->connect
207              
208             Establishes the connection to Sway. Returns an C which will
209             be triggered with a boolean (true if the connection was established) as soon as
210             the connection has been established.
211              
212             if ($sway->connect->recv) {
213             say "Connected to Sway";
214             }
215              
216             =cut
217             sub connect
218             {
219 2     2 1 14277 my ($self) = @_;
220 2         57 my $cv = AnyEvent->condvar;
221              
222             tcp_connect "unix/", $self->{path}, sub {
223 2     2   194 my ($fh) = @_;
224              
225 2 50       19 return $cv->send(0) unless $fh;
226              
227             $self->{ipchdl} = AnyEvent::Handle->new(
228             fh => $fh,
229 0         0 on_read => sub { my ($hdl) = @_; $self->_data_available($hdl) },
  0         0  
230             on_error => sub {
231 0         0 my ($hdl, $fatal, $msg) = @_;
232 0         0 delete $self->{ipchdl};
233 0         0 $hdl->destroy;
234              
235 0         0 my $cb = $self->{callbacks};
236              
237             # Trigger all one-time callbacks with undef
238 0         0 for my $type (keys %{$cb}) {
  0         0  
239 0 0       0 next if ($type & $event_mask) == $event_mask;
240 0         0 $cb->{$type}->();
241 0         0 delete $cb->{$type};
242             }
243              
244             # Trigger _error callback, if set
245 0         0 my $type = $events{_error};
246 0 0       0 return unless defined($cb->{$type});
247 0         0 $cb->{$type}->($msg);
248             }
249 0         0 );
250              
251 0         0 $cv->send(1)
252 2         79 };
253              
254 2         543 return $cv;
255             }
256              
257             sub _data_available
258             {
259 0     0     my ($self, $hdl) = @_;
260              
261             $hdl->unshift_read(
262             chunk => length($magic) + 4 + 4,
263             sub {
264 0     0     my $header = $_[1];
265             # Unpack message length and read the payload
266 0           my ($len, $type) = unpack("LL", substr($header, length($magic)));
267             $hdl->unshift_read(
268             chunk => $len,
269 0           sub { $self->_handle_sway_message($type, $_[1]) }
270 0           );
271             }
272 0           );
273             }
274              
275             sub _handle_sway_message
276             {
277 0     0     my ($self, $type, $payload) = @_;
278              
279 0 0         return unless defined($self->{callbacks}->{$type});
280              
281 0           my $cb = $self->{callbacks}->{$type};
282 0           $cb->(decode_json $payload);
283              
284 0 0         return if ($type & $event_mask) == $event_mask;
285              
286             # If this was a one-time callback, we delete it
287             # (when connection is lost, all one-time callbacks get triggered)
288 0           delete $self->{callbacks}->{$type};
289             }
290              
291             =head2 $sway->subscribe(\%callbacks)
292              
293             Subscribes to the given event types. This function awaits a hashref with the
294             key being the name of the event and the value being a callback.
295              
296             my %callbacks = (
297             workspace => sub { say "Workspaces changed" }
298             );
299              
300             if ($sway->subscribe(\%callbacks)->recv->{success}) {
301             say "Successfully subscribed";
302             }
303              
304             The special callback with name C<_error> is called when the connection to Sway
305             is killed (because of a crash, exit or restart of Sway most likely). You can
306             use it to print an appropriate message and exit cleanly or to try to reconnect.
307              
308             my %callbacks = (
309             _error => sub {
310             my ($msg) = @_;
311             say "I am sorry. I am so sorry: $msg";
312             exit 1;
313             }
314             );
315              
316             $sway->subscribe(\%callbacks)->recv;
317              
318             =cut
319             sub subscribe
320             {
321 0     0 1   my ($self, $callbacks) = @_;
322              
323             # Register callbacks for each message type
324 0           for my $key (keys %{$callbacks}) {
  0            
325 0           my $type = $events{$key};
326 0           $self->{callbacks}->{$type} = $callbacks->{$key};
327             }
328              
329 0           $self->message(TYPE_SUBSCRIBE, [ keys %{$callbacks} ])
  0            
330             }
331              
332             =head2 $sway->message($type, $content)
333              
334             Sends a message of the specified C to Sway, possibly containing the data
335             structure C (or C, encoded as utf8, if C is a
336             scalar), if specified.
337              
338             my $reply = $sway->message(TYPE_RUN_COMMAND, "reload")->recv;
339             if ($reply->{success}) {
340             say "Configuration successfully reloaded";
341             }
342              
343             =cut
344             sub message
345             {
346 0     0 1   my ($self, $type, $content) = @_;
347              
348 0 0         confess "No message type specified" unless defined($type);
349              
350 0 0         confess "No connection to Sway" unless defined($self->{ipchdl});
351              
352 0           my $payload = "";
353 0 0         if ($content) {
354 0 0         if (not ref($content)) {
355             # Convert from Perl’s internal encoding to UTF8 octets
356 0           $payload = encode_utf8($content);
357             } else {
358 0           $payload = encode_json $content;
359             }
360             }
361 0           my $message = $magic . pack("LL", length($payload), $type) . $payload;
362 0           $self->{ipchdl}->push_write($message);
363              
364 0           my $cv = AnyEvent->condvar;
365              
366             # We don’t preserve the old callback as it makes no sense to
367             # have a callback on message reply types (only on events)
368             $self->{callbacks}->{$type} =
369             sub {
370 0     0     my ($reply) = @_;
371 0           $cv->send($reply);
372 0           undef $self->{callbacks}->{$type};
373 0           };
374              
375 0           $cv
376             }
377              
378             =head1 SUGAR METHODS
379              
380             These methods intend to make your scripts as beautiful as possible. All of
381             them automatically establish a connection to Sway blockingly (if it does not
382             already exist).
383              
384             =cut
385              
386             sub _ensure_connection
387             {
388 0     0     my ($self) = @_;
389              
390 0 0         return if defined($self->{ipchdl});
391              
392 0 0         $self->connect->recv or confess "Unable to connect to Sway (socket path " . $self->{path} . ")";
393             }
394              
395             =head2 get_workspaces
396              
397             Gets the current workspaces from Sway.
398              
399             my $ws = sway->get_workspaces->recv;
400             say Dumper($ws);
401              
402             =cut
403             sub get_workspaces
404             {
405 0     0 1   my ($self) = @_;
406              
407 0           $self->_ensure_connection;
408              
409 0           $self->message(TYPE_GET_WORKSPACES)
410             }
411              
412             =head2 get_outputs
413              
414             Gets the current outputs from Sway.
415              
416             my $outs = sway->get_outputs->recv;
417             say Dumper($outs);
418              
419             =cut
420             sub get_outputs
421             {
422 0     0 1   my ($self) = @_;
423              
424 0           $self->_ensure_connection;
425              
426 0           $self->message(TYPE_GET_OUTPUTS)
427             }
428              
429             =head2 get_tree
430              
431             Gets the layout tree from Sway (>= v4.0).
432              
433             my $tree = sway->get_tree->recv;
434             say Dumper($tree);
435              
436             =cut
437             sub get_tree
438             {
439 0     0 1   my ($self) = @_;
440              
441 0           $self->_ensure_connection;
442              
443 0           $self->message(TYPE_GET_TREE)
444             }
445              
446             =head2 get_marks
447              
448             Gets all the window identifier marks from Sway (>= v4.1).
449              
450             my $marks = sway->get_marks->recv;
451             say Dumper($marks);
452              
453             =cut
454             sub get_marks
455             {
456 0     0 1   my ($self) = @_;
457              
458 0           $self->_ensure_connection;
459              
460 0           $self->message(TYPE_GET_MARKS)
461             }
462              
463             =head2 get_bar_config
464              
465             Gets the bar configuration for the specific bar id from Sway (>= v4.1).
466              
467             my $config = sway->get_bar_config($id)->recv;
468             say Dumper($config);
469              
470             =cut
471             sub get_bar_config
472             {
473 0     0 1   my ($self, $id) = @_;
474              
475 0           $self->_ensure_connection;
476              
477 0           $self->message(TYPE_GET_BAR_CONFIG, $id)
478             }
479              
480             =head2 get_version
481              
482             Gets the Sway version via IPC, with a fall-back that parses the output of Sway
483             --version (for Sway < v4.3).
484              
485             my $version = sway->get_version()->recv;
486             say "major: " . $version->{major} . ", minor = " . $version->{minor};
487              
488             =cut
489             sub get_version
490             {
491 0     0 1   my ($self) = @_;
492              
493 0           $self->_ensure_connection;
494              
495 0           my $cv = AnyEvent->condvar;
496              
497 0           my $version_cv = $self->message(TYPE_GET_VERSION);
498 0           my $timeout;
499             $timeout = AnyEvent->timer(
500             after => 1,
501             cb => sub {
502 0     0     warn "Falling back to sway --version since the running Sway doesn’t support GET_VERSION yet.";
503 0           my $version = _call_sway('--version');
504 0           $version =~ s/^sway version //;
505 0           my $patch = 0;
506 0           my ($major, $minor) = ($version =~ /^([0-9]+)\.([0-9]+)/);
507 0 0         if ($version =~ /^[0-9]+\.[0-9]+\.([0-9]+)/) {
508 0           $patch = $1;
509             }
510             # Strip everything from the © sign on.
511 0           $version =~ s/ ©.*$//g;
512 0           $cv->send({
513             major => int($major),
514             minor => int($minor),
515             patch => int($patch),
516             human_readable => $version,
517             });
518 0           undef $timeout;
519             },
520 0           );
521             $version_cv->cb(sub {
522 0     0     undef $timeout;
523 0           $cv->send($version_cv->recv);
524 0           });
525              
526 0           return $cv;
527             }
528              
529             =head2 get_config
530              
531             Gets the raw last read config from Sway. Requires Sway >= 4.14
532              
533             =cut
534             sub get_config
535             {
536 0     0 1   my ($self) = @_;
537              
538 0           $self->_ensure_connection;
539              
540 0           $self->message(TYPE_GET_CONFIG);
541             }
542              
543             =head2 send_tick
544              
545             Sends a tick event. Requires Sway >= 4.15
546              
547             =cut
548             sub send_tick
549             {
550 0     0 1   my ($self, $payload) = @_;
551              
552 0           $self->_ensure_connection;
553              
554 0           $self->message(TYPE_SEND_TICK, $payload);
555             }
556              
557             =head2 sync
558              
559             Sends an Sway sync event. Requires Sway >= 4.16
560              
561             =cut
562             sub sync
563             {
564 0     0 1   my ($self, $payload) = @_;
565              
566 0           $self->_ensure_connection;
567              
568 0           $self->message(TYPE_SYNC, $payload);
569             }
570              
571             =head2 command($content)
572              
573             Makes Sway execute the given command
574              
575             my $reply = sway->command("reload")->recv;
576             die "command failed" unless $reply->{success};
577              
578             =cut
579             sub command
580             {
581 0     0 1   my ($self, $content) = @_;
582              
583 0           $self->_ensure_connection;
584              
585 0           $self->message(TYPE_RUN_COMMAND, $content)
586             }
587              
588             =head1 AUTHOR
589              
590             John Mertz, C<< >>
591              
592             =head1 BUGS
593              
594             Please report any bugs or feature requests to C
595             rt.cpan.org>, or through the web interface at
596             L. I will be
597             notified, and then you'll automatically be notified of progress on your bug as
598             I make changes.
599              
600             =head1 SUPPORT
601              
602             You can find documentation for this module with the perldoc command.
603              
604             perldoc AnyEvent::Sway
605              
606             You can also look for information at:
607              
608             =over 2
609              
610             =item * RT: CPAN's request tracker
611              
612             L
613              
614             =item * The Sway window manager website
615              
616             L
617              
618             =back
619              
620              
621             =head1 ACKNOWLEDGEMENTS
622              
623              
624             =head1 LICENSE AND COPYRIGHT
625              
626             Copyright 2022 (C) John Mertz
627              
628             Forked from AnyEvent::I3 by Michael Stapelberg
629              
630             This program is free software; you can redistribute it and/or modify it
631             under the terms of either: the GNU General Public License as published
632             by the Free Software Foundation; or the Artistic License.
633              
634             See https://dev.perl.org/licenses/ for more information.
635              
636              
637             =cut
638              
639             1; # End of AnyEvent::Sway