]> git.sur5r.net Git - i3/i3/blob - i3-wsbar
Bugfix: i3-wsbar: properly catch errors when writing to child process
[i3/i3] / i3-wsbar
1 #!/usr/bin/env perl
2 # vim:ts=4:sw=4:expandtab:ft=perl
3 # © 2010 Michael Stapelberg, see LICENSE for license information
4
5 use strict;
6 use warnings;
7 use Getopt::Long;
8 use Pod::Usage;
9 use IPC::Run qw(start pump);
10 use Try::Tiny;
11 use AnyEvent::I3;
12 use AnyEvent;
13 use v5.10;
14
15 my $stdin;
16 my $i3 = i3;
17 my ($workspaces, $outputs) = ([], {});
18 my $last_line = "";
19
20 my $command = "";
21 my $input_on = "";
22 my $output_on = "";
23 my $show_all = 0;
24
25 my $result = GetOptions(
26     'command=s' => \$command,
27     'input-on=s' => \$input_on,
28     'output-on=s' => \$output_on,
29     'show-all' => \$show_all,
30     'help' => sub { pod2usage(1); exit 0 },
31 );
32
33 if ($command eq '') {
34     say "i3-wsbar is only useful in combination with dzen2.";
35     say "Please specify -c (command)";
36     exit 1;
37 }
38
39 my @input_on = split(/,/, $input_on);
40 my @output_on = split(/,/, $output_on);
41
42 # Disable buffering
43 $| = 1;
44
45 # Wait a short amount of time and try to connect to i3 again
46 sub reconnect {
47     my $timer;
48     my $c = sub {
49         $timer = AnyEvent->timer(
50             after => 0.01,
51             cb => sub { $i3->connect->cb(\&connected) }
52         );
53     };
54     $c->();
55 }
56
57 # Connection attempt succeeded or failed
58 sub connected {
59     my ($cv) = @_;
60
61     if (!$cv->recv) {
62         reconnect();
63         return;
64     }
65
66     $i3->subscribe({
67         workspace => \&ws_change,
68         output => \&output_change,
69         _error => sub { reconnect() }
70     });
71     ws_change();
72     output_change();
73 }
74
75 # Called when a ws changes
76 sub ws_change {
77     # Request the current workspaces and update the output afterwards
78     $i3->get_workspaces->cb(
79         sub {
80             my ($cv) = @_;
81             $workspaces = $cv->recv;
82             update_output();
83         });
84 }
85
86 # Called when the reply to the GET_OUTPUTS message arrives
87 # Compares old outputs with new outputs and starts/kills
88 # $command for each output (if specified)
89 sub got_outputs {
90     my $reply = shift->recv;
91     my %old = %{$outputs};
92     my %new = map { ($_->{name}, $_) } grep { $_->{active} } @{$reply};
93
94     # If no command was given, we do not need to compare outputs
95     if ($command eq '') {
96         update_output();
97         return;
98     }
99
100     # Handle new outputs
101     for my $name (keys %new) {
102         next if @output_on and !($name ~~ @output_on);
103
104         if (defined($old{$name})) {
105             # Check if the mode changed (by reversing the hashes so
106             # that we can check for equality using the smartmatch op)
107             my %oldrect = reverse %{$old{$name}->{rect}};
108             my %newrect = reverse %{$new{$name}->{rect}};
109             next if (%oldrect ~~ %newrect);
110
111             # On mode changes, we re-start the command
112             $outputs->{$name}->{cmd}->finish;
113             delete $outputs->{$name};
114         }
115
116         my $x = $new{$name}->{rect}->{x};
117         my $launch = $command;
118         $launch =~ s/([^%])%x/$1$x/g;
119         $launch =~ s/%%x/%x/g;
120
121         $new{$name}->{cmd_input} = '';
122         my @cmd = ('/bin/sh', '-c', $launch);
123         $new{$name}->{cmd} = start \@cmd, \$new{$name}->{cmd_input};
124         $outputs->{$name} = $new{$name};
125     }
126
127     # Handle old outputs
128     for my $name (keys %old) {
129         next if defined($new{$name});
130
131         $outputs->{$name}->{cmd}->finish;
132         delete $outputs->{$name};
133     }
134
135     update_output();
136 }
137
138 sub output_change {
139     $i3->get_outputs->cb(\&got_outputs)
140 }
141
142 sub update_output {
143     my $dzen_bg = "#111111";
144     my $out;
145
146     for my $name (keys %{$outputs}) {
147         my $width = $outputs->{$name}->{rect}->{width};
148
149         $out = qq|^pa(;2)|;
150         for my $ws (@{$workspaces}) {
151             next if $ws->{output} ne $name and !$show_all;
152
153             my ($bg, $fg) = qw(333333 888888);
154             ($bg, $fg) = qw(4c7899 ffffff) if $ws->{visible};
155             ($bg, $fg) = qw(900000 ffffff) if $ws->{urgent};
156
157             my $cmd = q|i3-msg "| . $ws->{num} . q|"|;
158             my $name = $ws->{name};
159
160             # Begin the clickable area
161             $out .= qq|^ca(1,$cmd)|;
162
163             # Draw the rest of the bar in the background color, but
164             # don’t move the "cursor"
165             $out .= qq|^p(_LOCK_X)^fg(#$bg)^r(${width}x17)^p(_UNLOCK_X)|;
166             # Draw the name of the workspace without overwriting the
167             # background color
168             $out .= qq|^p(+3)^fg(#$fg)^ib(1)$name^ib(0)^p(+5)|;
169             # Draw the rest of the bar in the normal background color
170             # without moving the "cursor"
171             $out .= qq|^p(_LOCK_X)^fg($dzen_bg)^r(${width}x17)^p(_UNLOCK_X)|;
172
173             # End the clickable area
174             $out .= qq|^ca()|;
175
176             # Move to the next rect, reset Y coordinate
177             $out .= qq|^p(2)^pa(;2)|;
178         }
179
180         $out .= qq|^p(_LOCK_X)^fg($dzen_bg)^r(${width}x17)^p(_UNLOCK_X)^fg(white)|;
181         $out .= qq|^p(+5)|;
182         $out .= $last_line if (!@input_on or $name ~~ @input_on);
183         $out .= "\n";
184
185         $outputs->{$name}->{cmd_input} = $out;
186         try {
187             pump $outputs->{$name}->{cmd} while length $outputs->{$name}->{cmd_input};
188         } catch {
189             warn "Could not write to dzen2";
190             exit 1;
191         }
192     }
193 }
194
195 $i3->connect->cb(\&connected);
196
197 $stdin = AnyEvent->io(
198     fh => \*STDIN,
199     poll => 'r',
200     cb => sub {
201         chomp (my $line = <STDIN>);
202         $last_line = $line;
203         update_output();
204     });
205
206 # let AnyEvent do the rest ("endless loop")
207 AnyEvent->condvar->recv
208
209 __END__
210
211 =head1 NAME
212
213 i3-wsbar - sample implementation of a standalone workspace bar
214
215 =head1 SYNOPSIS
216
217 i3-wsbar -c <dzen2-commandline> [options]
218
219 =head1 OPTIONS
220
221 =over 4
222
223 =item B<--command> <command>
224
225 This command (at the moment only dzen2 is supported) will be started for each
226 output. C<%x> will be replaced with the X coordinate of the output.
227
228 Example:
229     --command "dzen2 -dock -x %x"
230
231 =item B<--input-on> <list-of-RandR-outputs>
232
233 Specifies on which outputs the contents of stdin should be appended to the
234 workspace bar.
235
236 Example:
237     --input-on "LVDS1"
238
239 =item B<--output-on> <list-of-RandR-outputs>
240
241 Specifies for which outputs i3-wsbar should start C<command>.
242
243 =item B<--show-all>
244
245 If enabled, all workspaces are shown (not only those of the current output).
246 Handy to use with C<--output-on>.
247
248 =back
249
250 =cut