File Coverage

blib/lib/AnyEvent/I3X/Workspace/OnDemand.pm
Criterion Covered Total %
statement 32 305 10.4
branch 0 84 0.0
condition 0 13 0.0
subroutine 11 42 26.1
pod 9 20 45.0
total 52 464 11.2


line stmt bran cond sub pod time code
1             package AnyEvent::I3X::Workspace::OnDemand;
2             our $VERSION = '0.010';
3 2     2   529683 use v5.26;
  2         8  
4 2     2   1648 use Object::Pad;
  2         21502  
  2         27  
5              
6             # ABSTRACT: An I3 workspace loader
7              
8             class AnyEvent::I3X::Workspace::OnDemand;
9 2     2   833 use Carp qw(croak);
  2         5  
  2         106  
10              
11 2     2   1285 use AnyEvent::I3 qw(:all);
  2         164283  
  2         647  
12 2     2   24 use List::Util qw(first any);
  2         4  
  2         192  
13 2     2   1216 use File::Spec::Functions qw(catfile);
  2         1579  
  2         144  
14 2     2   1021 use Data::Compare;
  2         31151  
  2         36  
15 2     2   10302 use Data::Dumper;
  2         22414  
  2         339  
16 2     2   2153 use X11::Protocol;
  2         66532  
  2         162  
17 2     2   1233 use Proc::ProcessTable;
  2         11046  
  2         109  
18 2     2   971 use IO::Select;
  2         4269  
  2         24375  
19              
20             field $i3;
21             field $layout_path : param = catfile($ENV{HOME}, qw(.config i3));
22              
23             field @groups;
24             field $starting_group :param = undef;
25             field $starting_workspace :param = undef;
26             field $debug :param = 0;
27              
28             field $log_all_events :param = undef;
29              
30             field $socket :param = undef;
31             field $i3status :param = 1;
32              
33             field %workspace;
34             field %output;
35             field %mode;
36             field %window;
37             field %barconfig_update;
38             field %binding;
39             field %tick;
40             field %shutdown;
41              
42             field @swallows;
43             field $c;
44              
45             field $current_group;
46             field $current_workspace;
47              
48             field $x11;
49             field $xroot;
50              
51             ADJUSTPARAMS {
52             my $args = shift;
53              
54             $debug = 1 if $log_all_events;
55              
56             # i3
57             %workspace = %{ delete $args->{workspace} }
58             if ref $args->{workspace} eq 'HASH';
59             %barconfig_update = %{ delete $args->{barconfig_update} }
60             if ref $args->{barconfig_update} eq 'HASH';
61              
62             %tick = %{ delete $args->{tick} } if ref $args->{tick} eq 'HASH';
63             %shutdown = %{ delete $args->{shutdown} } if ref $args->{shutdown} eq 'HASH';
64             %output = %{ delete $args->{output} } if ref $args->{output} eq 'HASH';
65             %mode = %{ delete $args->{mode} } if ref $args->{mode} eq 'HASH';
66             %window = %{ delete $args->{window} } if ref $args->{window} eq 'HASH';
67             %binding = %{ delete $args->{binding} } if ref $args->{binding} eq 'HASH';
68              
69             # us
70             @groups = @{ delete $args->{groups} } if ref $args->{groups} eq 'ARRAY';
71             @swallows = @{ delete $args->{swallows} }
72             if ref $args->{swallows} eq 'ARRAY';
73             }
74              
75 0     0 0   method log_event($type, $event) {
  0            
  0            
  0            
  0            
76              
77 0           my $msg;
78 0 0         if ($type eq 'tick') {
    0          
79 0           $msg = "Processing tick with payload $event->{payload}";
80             }
81             elsif ($type eq 'workspace') {
82 0           $msg = "Processing workspace event $event->{change} on $event->{current}{name}";
83             }
84             else {
85 0           $msg = "Processing $type with payload $event->{change}";
86             }
87              
88 0           $self->log($msg);
89              
90 0 0         return unless $log_all_events;
91              
92 0           open my $fh, '>>', $log_all_events;
93 0           print $fh join($/, $msg, Dumper $event, "");
94 0           close($fh);
95             }
96              
97 0     0 0   method x11() {
  0            
  0            
98              
99 0 0         unless ($x11) {
100 0           $x11 = X11::Protocol->new();
101 0           $xroot = $x11->root;
102 0           return $x11;
103             }
104              
105 0           my $fh = $x11->connection->fh;
106 0           my $sel = IO::Select->new($fh);
107              
108 0 0         if ($sel->can_read(0)) {
109 0           eval { $x11->handle_input() };
  0            
110 0 0         $x11 = X11::Protocol->new() if $@;
111             }
112 0           $xroot = $x11->root;
113 0           return $x11;
114              
115             }
116              
117 0     0     method _get_string_property_from_root_window($key, $atom_type) {
  0            
  0            
  0            
  0            
118 0           my $prop = $self->x11->atom($key);
119 0           my $prop_type = $self->x11->atom($atom_type);
120              
121 0           my ($value, $type, $format, $bytes_after)
122             = $x11->GetProperty($xroot, $prop, $prop_type, 0, 1024);
123              
124 0 0         return $value if $value;
125 0           return;
126             }
127              
128 0     0     method _get_property_from_root_window($key) {
  0            
  0            
  0            
129 0           my $value = $self->_get_string_property_from_root_window($key, 'UTF8_STRING');
130 0 0         return $value if defined $value;
131             # Allow xprop values to be set but also allow
132 0           $value = $self->_get_string_property_from_root_window($key, 'STRING');
133 0 0         return unless defined $value;
134 0           $self->_set_property_on_root_window($key, $value);
135 0           return $value;
136             }
137              
138 0     0     method _set_property_on_root_window($key, $value) {
  0            
  0            
  0            
  0            
139 0           my $prop = $self->x11->atom($key);
140 0           my $utf8 = $self->x11->atom('UTF8_STRING');
141              
142 0           $x11->ChangeProperty($xroot, $prop, $utf8, 8, 'Replace', $value);
143 0           $x11->flush;
144             }
145              
146 0     0 0   method set_group_on_root_window($name) {
  0            
  0            
  0            
147 0           $self->_set_property_on_root_window('_I3_WOD_GROUP', $name);
148              
149 0 0         return unless $i3status;
150              
151             # If we do this we should send a sigusr1 to i3status
152 0           my $pt = Proc::ProcessTable->new(enable_ttys => 0);
153 0           my @pids = grep { $_->exec eq '/usr/bin/i3status' } @{ $pt->table };
  0            
  0            
154 0           kill('USR1', $_->pid) foreach @pids;
155             }
156              
157 0     0 0   method get_group_from_root_window() {
  0            
  0            
158 0           my $group = $self->_get_property_from_root_window('_I3_WOD_GROUP');
159 0 0         return $group if $group;
160 0           $self->set_group_on_root_window($groups[0]);
161 0           return $groups[0];
162             }
163              
164 0     0 0   method set_workspace_on_root_window($name) {
  0            
  0            
  0            
165 0           $self->_set_property_on_root_window('_I3_WOD_WORKSPACE', $name);
166             }
167              
168 0     0 0   method get_workspace_from_root_window() {
  0            
  0            
169 0           my $ws = $self->_get_property_from_root_window('_I3_WOD_WORKSPACE');
170 0 0         return $ws if $ws;
171 0           return;
172             }
173              
174              
175              
176             ADJUST {
177              
178             $i3 = $socket ? i3($socket) : i3();
179             $i3->connect->recv or die "Error connecting to i3";
180              
181             $c = Data::Compare->new();
182              
183             $current_group = $self->get_group_from_root_window();
184             $current_workspace = $self->get_workspace_from_root_window();
185             $self->workspace($starting_workspace) unless $current_workspace;
186              
187             my $name;
188              
189             $self->subscribe(
190             workspace => sub {
191              
192             my $event = shift;
193             my $type = $event->{change};
194              
195             $current_workspace = $event->{current}{name};
196             $name = $current_workspace;
197              
198             $self->set_workspace_on_root_window($name) unless $type eq 'empty';
199              
200             $self->log_event('workspace', $event);
201              
202             # It doesn't have anything, skip skip next;
203             return if $type eq 'reload';
204              
205             return unless %workspace;
206             # Don't allow access to workspace which aren't part of the current group
207             if (exists $workspace{$name}{group}
208             && !$self->_is_in_group($name, $current_group)) {
209             if ($event->{old}{name}) {
210             $self->workspace($event->{old}{name});
211             return;
212             }
213              
214             # it is strange that we don't have an old workspace here...
215             warn
216             "Unable to determine old workspace, but group hasn't defined a workspace",
217             $/;
218             }
219              
220             my $layout = $workspace{$name}{layout};
221             if ($layout) {
222             if ($type eq 'init') {
223             $layout = $self->_get_layout($name, $current_group);
224             if ($layout) {
225             $self->append_layout($name, $layout_path, $layout);
226             $self->start_apps_of_layout($name);
227             }
228              
229              
230             }
231             elsif ($type eq 'focus') {
232             $self->start_apps_of_layout($name);
233             }
234             }
235              
236             if (my $sub = $workspace{$name}{$type}) {
237             $sub->($self, $i3, $event);
238             }
239             }
240             );
241              
242             $self->subscribe(
243             tick => sub {
244              
245             my $event = shift;
246             my $payload = $event->{payload};
247              
248             $payload = "__EMPTY__" unless length($payload);
249             $event->{payload} = $payload;
250              
251             $self->log_event('tick', $event);
252              
253             if ($payload =~ /^group:([[:word:]]+)$/) {
254             # Skip if we have no groups
255             return unless any { $_ eq $1 } @groups;
256             $self->switch_to_group($1);
257             return;
258             }
259              
260             return unless %tick;
261              
262             if (my $sub = $tick{$payload}) {
263             $sub->($self, $i3, $event);
264             }
265             }
266             );
267              
268             $self->subscribe(
269             shutdown => sub {
270             my $event = shift;
271             $self->log_event('shutdown', $event);
272              
273             my $payload = $event->{change};
274             if (my $sub = $shutdown{$payload}) {
275             $sub->($self, $i3, $event);
276             }
277             }
278             );
279              
280             $self->subscribe(
281             barconfig_update => sub {
282             my $event = shift;
283              
284             $self->log_event('barconfig_update', $event);
285              
286             # This event consists of a single serialized map reporting on options
287             # from the barconfig of the specified bar_id that were updated in i3.
288             # This event is the same as a GET_BAR_CONFIG reply for the bar with the
289             # given id.
290             warn "barconfig_update is currently not supported", $/
291             if %barconfig_update;
292             }
293             );
294              
295             $self->subscribe(
296             output => sub {
297             my $event = shift;
298             $self->log_event('output', $event);
299              
300             my $payload = $event->{change};
301             if (my $sub = $output{$payload}) {
302             $sub->($self, $i3, $event);
303             }
304             }
305             );
306             $self->subscribe(
307             mode => sub {
308             my $event = shift;
309             $self->log_event('mode', $event);
310              
311             my $payload = $event->{change};
312             if (my $sub = $mode{$payload}) {
313             $sub->($self, $i3, $event);
314             }
315             }
316             );
317             $self->subscribe(
318             window => sub {
319             my $event = shift;
320             $self->log_event('window', $event);
321              
322             my $payload = $event->{change};
323             if (my $sub = $window{$payload}) {
324             $sub->($self, $i3, $event);
325             }
326             }
327             );
328             $self->subscribe(
329             binding => sub {
330             my $event = shift;
331             $self->log_event('binding', $event);
332             my $payload = $event->{change};
333             if (my $sub = $binding{$payload}) {
334             $sub->($self, $i3, $event);
335             }
336             }
337             );
338              
339             }
340              
341 0     0     method _is_in_group ($name, $group) {
  0            
  0            
  0            
  0            
342 0           my $ws = $workspace{$name};
343 0 0         return 0 unless $ws;
344 0 0         return 0 unless exists $ws->{group};
345 0 0         return 1 if exists $ws->{group}{$group};
346 0 0         return 1 if exists $ws->{group}{all};
347             }
348              
349 0     0     method _get_layout ($name, $group) {
  0            
  0            
  0            
  0            
350 0           my $ws = $workspace{$name};
351              
352 0 0         return unless $ws;
353              
354 0 0         return $ws->{layout} unless exists $ws->{group};
355 0 0         return unless $self->_is_in_group($name, $group);
356             return $ws->{group}{$group}{layout} // $ws->{group}{all}{layout}
357 0   0       // $ws->{layout};
      0        
358             }
359              
360 0     0 0   method switch_to_group ($group) {
  0            
  0            
  0            
361              
362 0           my $cur = $current_workspace;
363 0 0 0       return if $current_group eq $group && $cur ne '__EMPTY__';
364              
365             $i3->get_workspaces->cb(
366             sub {
367 0     0     my $y = shift;
368 0           my $x = $y->recv;
369 0           my @current_workspaces = @$x;
370              
371 0 0         if ($cur eq '__EMPTY__') {
372 0           ($cur) = map { $_->{name} } grep { $_->{focused} } @current_workspaces;
  0            
  0            
373 0           $current_workspace = $cur;
374 0 0         return if $current_group eq $group;
375             }
376              
377 0           my $qr = qr/^$group\:.+/;
378 0           my @available = grep { /^$qr/ } map { $_->{name} } @$x;
  0            
  0            
379              
380 0           foreach my $name (keys %workspace) {
381 0           my $ws = $workspace{$name};
382 0 0         next unless exists $ws->{group};
383              
384 0 0         if (any { $name eq $_->{name}} @current_workspaces) {
  0            
385 0 0         if ($self->_is_in_group($name, $current_group)) {
386 0           $self->workspace($name, "rename workspace to $current_group:$name");
387             }
388             }
389              
390 0 0         if (any { "$group:$name" eq $_ } @available) {
  0            
391 0           $self->workspace("$group:$name", "rename workspace to $name");
392             }
393             }
394              
395 0           $current_group = $group;
396 0           $self->set_group_on_root_window($group);
397 0           $self->workspace($cur);
398             }
399 0           );
400              
401              
402             }
403              
404 0     0 1   method log ($msg) {
  0            
  0            
  0            
405 0 0         return unless $debug;
406 0           warn $msg, $/;
407 0           return;
408             }
409              
410 0     0 1   method debug ($d = undef) {
  0            
  0            
  0            
411 0 0         return $debug unless defined $d;
412 0           $debug = $d;
413             }
414              
415             my @any = qw(any *);
416              
417 0     0 1   method on_workspace ($name, $type, $sub) {
  0            
  0            
  0            
  0            
  0            
418              
419 0 0         if (ref $sub ne 'CODE') {
420 0           croak("Please supply a code ref!");
421             }
422              
423 0           state @actions = qw(init focus empty urgent reload rename restored move);
424              
425 0 0   0     if (any { $_ eq $type } @any) {
  0 0          
426 0           $workspace{$name}{$_} = $sub for @actions;
427             }
428 0     0     elsif (any { $_ eq $type } @actions) {
429 0           $workspace{$name}{$type} = $sub;
430             }
431             else {
432 0           croak("Unsupported action '$type', please use any of the following:"
433             . join(", ", @actions));
434             }
435             }
436              
437 0     0 0   method on_shutdown ($payload, $sub) {
  0            
  0            
  0            
  0            
438 0 0         if (ref $sub ne 'CODE') {
439 0           croak("Please supply a code ref!");
440             }
441 0           state @payloads = qw(exit restart);
442 0 0   0     if (any { $_ eq $payload } @any) {
  0 0          
443 0           $shutdown{$_} = $sub for @payloads;
444             }
445 0     0     elsif (any { $_ eq $payload } @payloads) {
446 0           $shutdown{$payload} = $sub;
447             }
448             else {
449 0           croak("Unsupported action '$payload', please use any of the following:"
450             . join(", ", @payloads));
451             }
452             }
453              
454 0     0 1   method on_tick ($payload, $sub) {
  0            
  0            
  0            
  0            
455 0 0         if (ref $sub ne 'CODE') {
456 0           croak("Please supply a code ref!");
457             }
458 0           $tick{$payload} = $sub;
459             }
460              
461 0     0 1   method add_swallow ($match, $cmd, $on = undef) {
  0            
  0            
  0            
  0            
  0            
462 0 0         push(
463             @swallows,
464             {
465             match => $match,
466             cmd => $cmd,
467             defined $on ? (on => $on) : (),
468             }
469             );
470             }
471              
472 0     0 1   method subscribe ($action, $sub) {
  0            
  0            
  0            
  0            
473 0           my $answer = $i3->subscribe({ $action => $sub });
474 0           $answer->send;
475 0           return;
476             }
477              
478 0     0 1   method get_i3() {
  0            
  0            
479 0           return $i3;
480             }
481              
482 0     0 1   method command (@args) {
  0            
  0            
  0            
483 0           $i3->command(join(" ", @args));
484             }
485              
486 0     0 0   method append_layout ($name, @layout) {
  0            
  0            
  0            
  0            
487 0           my $layout = catfile(@layout);
488 0           $self->workspace($name, "append_layout $layout");
489             }
490              
491 0     0 1   method workspace ($workspace, @rest) {
  0            
  0            
  0            
  0            
492 0           $self->command(join(";", "workspace $workspace", @rest));
493             }
494              
495 0     0 0   method swallow_to_exec ($name, $node) {
  0            
  0            
  0            
  0            
496              
497 0           my @targets = @{ $node->{swallows} };
  0            
498              
499 0 0         if (!@targets) {
500 0           $self->swallow_to_exec($name, $_) foreach @{ $node->{nodes} };
  0            
501 0           return;
502             }
503              
504 0           my @cmds;
505 0           foreach (@swallows) {
506 0 0         if (!exists $_->{on}) {
507 0           push(@cmds, $_);
508 0           next;
509             }
510 0 0 0       if (($_->{on}{workspace} // '') eq $current_workspace) {
511 0           push(@cmds, $_);
512 0           next;
513             }
514 0 0 0       if(($_->{on}{group} // '') eq $current_group) {
515 0           push(@cmds, $_);
516 0           next;
517             }
518             }
519              
520             $self->command("exec $_")
521 0           for map { $_->{cmd} =~ s/^(?:exec\b\s+)//r; }
  0            
522 0           grep { $c->Cmp($targets[0], $_->{match}) } @cmds;
523             }
524              
525 0     0 0   method start_apps_of_layout ($name) {
  0            
  0            
  0            
526             $i3->get_tree->cb(
527             sub {
528 0     0     my $x = shift;
529 0           my $tree = $x->recv;
530 0           my $nodes = $tree->{nodes};
531 0           foreach (@{$nodes}) {
  0            
532 0 0         next if $_->{name} eq '__i3';
533 0           my $node = first { $_->{name} eq 'content' } @{ $_->{nodes} };
  0            
  0            
534 0           my $ws = first { $_->{name} eq $name } @{ $node->{nodes} };
  0            
  0            
535 0 0         next unless $ws;
536 0           $self->swallow_to_exec($name, $_) foreach @{ $ws->{nodes} };
  0            
537             }
538             }
539 0           );
540             }
541              
542             1;
543              
544             __END__