File Coverage

blib/lib/AnyEvent/I3X/Workspace/OnDemand.pm
Criterion Covered Total %
statement 29 277 10.4
branch 0 74 0.0
condition 0 13 0.0
subroutine 10 39 25.6
pod 9 19 47.3
total 48 422 11.3


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