From c5ce3773ad1b8397365e0239f6f0353d4d0ab17e Mon Sep 17 00:00:00 2001 From: Eleksir Date: Fri, 9 Apr 2021 16:13:25 +0300 Subject: [PATCH] Initial commit --- LICENSE | 21 ++ cpanfile | 7 + i3status-anyevent | 824 +++++++++++++++++++++++++++++++++++++++++ i3status-anyevent.json | 294 +++++++++++++++ 4 files changed, 1146 insertions(+) create mode 100644 LICENSE create mode 100644 cpanfile create mode 100644 i3status-anyevent create mode 100644 i3status-anyevent.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2dbc9df --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Sergei Fedosoff + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cpanfile b/cpanfile new file mode 100644 index 0000000..35a51d3 --- /dev/null +++ b/cpanfile @@ -0,0 +1,7 @@ +requires 'AnyEvent'; +requires 'AnyEvent::Timer::Cron'; +requires 'AnyEvent::I3'; +requires 'JSON::XS'; +requires 'Encode'; +requires 'IO::Socket'; +requires 'POSIX'; diff --git a/i3status-anyevent b/i3status-anyevent new file mode 100644 index 0000000..02e4104 --- /dev/null +++ b/i3status-anyevent @@ -0,0 +1,824 @@ +#!/usr/bin/perl + +# TODO: on i3wm reload (i3status restart), restore active windows status +# TODO: make battery plugin take into account charge level, add ability to call suspend2disk on critical charge level +# TODO: More complex PA-plugin with volume adjustment of default sink + +# we need to disable hash shuffling in order to make as less prints to stdout as possible +BEGIN { + unless (defined $ENV{PERL_PERTURB_KEYS}) { + $ENV{PERL_PERTURB_KEYS} = 0; + exec ( $^X, $0, @ARGV ); + exit 0; # we actually never get here + } +} + +use 5.018; +use strict; +use warnings; +use utf8; +use open qw (:std :utf8); +use Fcntl qw (:seek); +use AnyEvent; +use AnyEvent::Timer::Cron; +use AnyEvent::I3; +use JSON::XS; +use Encode; +use IO::Socket qw (AF_INET SOCK_STREAM SHUT_WR); +use POSIX qw (setsid); + +local $| = 1; + +sub add_events (); +sub spawn (@); +sub find_caps_led (); +sub to_hours ($); +sub readconf (); +sub active_win_borders (); + +my $CONF = readconf (); +my $i3 = i3 (); +my $done = AnyEvent->condvar; +$i3->connect->recv or die 'Unable to connect to i3'; + +# event pool +my @EP; + +# reload config and re-apply it on SIGHUP +local $SIG{HUP} = sub { + $CONF = readconf (); + active_win_borders (); + $#EP = -1; + add_events (); +}; + +my @month = ('Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'); +my @week_day = ('Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'); + +# File handler pool +my $LAHANDLE; +my $MEMINFOHANDLE; +my $CPUTEMPHANDLE; +my $CAPSHANDLE; + +# everything for text labels on i3bar +my $wallclock = ''; +my $la = ''; +my $meminfo = ''; +my $cputemp = ''; +my $caps_state = ''; +my $charge = ''; +my $tcp_check = 0; +my $file_check = 0; +my $file_check_mtime = 0; +my $simple_volume_pa = '♪ ??%'; +my $interfaces = '??'; +my $output_string; +my $old_output_string = ''; +my $STDIN_FIRST_LINE_DROPPED = 0; +# hp, kernel 5.4.x +my $BATTERY_CHARGE_NOW_FILE = sprintf '%s/charge_now', $CONF->{battery}->{sys_dir}; + +unless (-f $BATTERY_CHARGE_NOW_FILE) { + # lenovo, kernel 4.4.x + $BATTERY_CHARGE_NOW_FILE = sprintf '%s/energy_now', $CONF->{battery}->{sys_dir}; +} + +# hp, kernel 5.4.x +my $BATTERY_CHARGE_FULL_FILE = sprintf '%s/charge_full', $CONF->{battery}->{sys_dir}; + +unless (-f $BATTERY_CHARGE_FULL_FILE) { + # lenovo, kernel 4.4.x + $BATTERY_CHARGE_FULL_FILE = sprintf '%s/energy_full', $CONF->{battery}->{sys_dir}; +} + +my $CAPS_LED_PATH = find_caps_led (); + +# opened windows info storage ref +my $win; + +# capture click events and do not override sigstop and sigcont numbers +print << 'EOJSON'; +{"version": 1, "click_events": true} +[ [], +EOJSON + +# i3 events should persist over config relads +my $i3_subscription->{window} = sub { + my $w = shift; + + if (defined $w->{change}) { + if ($w->{change} eq 'new' ) { + if (defined $w->{container}->{window_properties}->{instance}) { + if (defined $win->{instance}->{$w->{container}->{window_properties}->{instance}}) { + $win->{instance}->{$w->{container}->{window_properties}->{instance}}++; + } else { + $win->{instance}->{$w->{container}->{window_properties}->{instance}} = 1; + } + } + } elsif ($w->{change} eq 'close') { + if (defined $w->{container}->{window_properties}->{instance}) { + if (defined $win->{instance}->{$w->{container}->{window_properties}->{instance}}) { + if ($win->{instance}->{$w->{container}->{window_properties}->{instance}} > 1) { + $win->{instance}->{$w->{container}->{window_properties}->{instance}} --; + } else { + delete $win->{instance}->{$w->{container}->{window_properties}->{instance}}; + } + } + } + } + + active_win_borders (); + } +}; + +$i3_subscription->{_error} = sub { + my ($msg) = @_; + die "Connection to i3 closed: $msg"; +}; + +$i3->subscribe($i3_subscription)->recv->{success} || die 'Unable to subscribe to i3 events'; + +# now we can add other events +add_events (); + +# and start eventloop +$done->recv (); +exit 0; + + +sub add_events () { + # Register callbacks/event handlers + push @EP, AnyEvent->timer ( + after => 1, + interval => 120, + cb => sub { + AnyEvent->now_update; + }, + ); + + # Actually print messages there + push @EP, AnyEvent->timer ( + after => 1, # sleep 1 second + interval => 0.3, # then invoke every ~1/3 second + cb => sub { # the callback to invoke + my $OUTPUT; + + if ($CONF->{app_buttons}) { + @{$OUTPUT} = @{$CONF->{apps}}; + } + + if ($CONF->{capslock}->{enabled}) { + if ($caps_state) { + push @{$OUTPUT}, { + background => $CONF->{capslock}->{color}, + color => $CONF->{capslock}->{background}, + border => $CONF->{capslock}->{background}, + full_text => 'caps', + markup => 'pango' + }; + } else { + push @{$OUTPUT}, { + color => $CONF->{capslock}->{color}, + background => $CONF->{capslock}->{background}, + border => $CONF->{capslock}->{background}, + full_text => 'caps', + markup => 'pango' + }; + } + } + + if ($CONF->{cpu_temp}->{enabled}) { + push @{$OUTPUT}, { color => $CONF->{color}, full_text => $cputemp }; + } + + if ($CONF->{mem}) { + push @{$OUTPUT}, { color => $CONF->{color}, full_text => $meminfo }; + } + + if ($CONF->{la}) { + push @{$OUTPUT}, { color => $CONF->{color}, full_text => "LA: $la" }; + } + + my $vpn_str = 'VPN:'; + + if ($CONF->{vpn}->{enabled}) { + if ($file_check) { + $vpn_str .= '⍋'; + } else { + $vpn_str .= '⍒'; + } + } + + if ($CONF->{vpn}->{tcp_check}->{enabled}) { + if ($CONF->{vpn}->{enabled}) { + $vpn_str .= ':'; + } + + if ($tcp_check) { + $vpn_str .= '✔'; + } else { + $vpn_str .= '✘'; + } + } + + if ($CONF->{vpn}->{tcp_check}->{enabled} || $CONF->{vpn}->{enabled}) { + push @{$OUTPUT}, { color => $CONF->{color}, full_text => $vpn_str }; + } + + if ($CONF->{'net-if'}->{enabled}) { + push @{$OUTPUT}, { color => $CONF->{color}, full_text => $interfaces, markup => 'pango' }; + } + + if ($CONF->{battery}->{enabled}) { + push @{$OUTPUT}, { color => $CONF->{color}, full_text => $charge, markup => 'pango' }; + } + + if ($CONF->{'simple-volume-pa'}) { + push @{$OUTPUT}, { + color => $CONF->{color}, + full_text => sprintf ('♪ %s%%', $simple_volume_pa), + markup => 'pango' + }; + } + + if ($CONF->{clock}->{enabled}) { + push @{$OUTPUT}, { name => 'wallclock', color => $CONF->{clock}->{color}, full_text => "$wallclock", markup => 'pango'}; + } + + $output_string = eval { encode_json $OUTPUT }; + + if (defined $output_string) { + if ($output_string ne $old_output_string) { + print decode ('UTF-8', $output_string) . ",\n"; + $old_output_string = $output_string; + } + } else { + warn $@; + } + + @{$OUTPUT} = -1; + } + ); + + + # read STDIN + push @EP, AnyEvent->io ( + fh => \*STDIN, + poll => 'r', + cb => sub { + my $msg = <>; + + unless ($STDIN_FIRST_LINE_DROPPED) { + $msg = <>; + $STDIN_FIRST_LINE_DROPPED = 1; + } + + if ($msg) { + # drop leading ',' if present + if (substr ($msg, 0, 1) eq ',') { + $msg = substr $msg, 1; + } + + my $j = eval { decode_json $msg }; + + if (defined $j) { + # left button click, without shift, ctrl, alt, etc + if (($j->{button} == 1) && ($#{$j->{modifiers}} < 0)) { + foreach (@{$CONF->{apps}}) { + unless (defined $_->{name}) { + next; + } + + unless (defined $j->{name}) { + next; + } + + if ($j->{name} eq $_->{name}) { + spawn ($_->{cmd}); + last; + } + } + } + + if (($j->{name} eq 'wallclock') && ($CONF->{clock}->{enabled})) { + if ($CONF->{clock}->{left_click}->{enabled}) { + if (($j->{button} == 1) && ($#{$j->{modifiers}} < 0)) { + spawn ($CONF->{clock}->{left_click}->{cmd}); + } + } elsif ($CONF->{clock}->{right_click}->{enabled}) { + if (($j->{button} == 2) && ($#{$j->{modifiers}} < 0)) { + spawn ($CONF->{clock}->{right_click}->{cmd}); + } + } + } + } + } + } + ); + + # poll caps lock status, on one of my laptop there is no led for this :( + if ($CONF->{capslock}->{enabled}) { + push @EP, AnyEvent->timer ( + after => 0, # first invoke ASAP + interval => 0.3, # then invoke every 0.3 seconds + cb => sub { # the callback to invoke + if (defined $CAPS_LED_PATH) { + if (defined $CAPSHANDLE) { + if (tell ($CAPSHANDLE) == -1) { + unless (open $CAPSHANDLE, '<', $CAPS_LED_PATH) { + $CAPS_LED_PATH = undef; + $caps_state = 2; + } + } + } else { + unless (open $CAPSHANDLE, '<', $CAPS_LED_PATH) { + $CAPS_LED_PATH = undef; + $caps_state = 2 + } + } + } else { + $caps_state = 2; + } + + if ($caps_state ne '2') { + $caps_state = eval { readline ($CAPSHANDLE) }; + seek $CAPSHANDLE, SEEK_SET, 0; + chomp $caps_state; + } + } + ); + } + + # poll clock by timer + if ($CONF->{clock}->{enabled}) { + push @EP, AnyEvent->timer ( + after => 0, # first invoke ASAP + interval => 1, # then invoke every 1 second + cb => sub { # the callback to invoke + # Fetch time for clock + my ($sec, $min, $hour, $mday, $mon, $year, $wday) = (localtime)[0,1,2,3,4,5,6]; + $year = $year + 1900; + $wallclock = sprintf ' %s, %s %s %s %02s:%02s ', $week_day[$wday], $mday, $month[$mon], $year, $hour, $min; + } + ); + } + + # poll battery status via upower or /sys + if ($CONF->{battery}->{enabled}) { + # borrow data from upower output + if ($CONF->{battery}->{driver} eq 'upower') { + push @EP, AnyEvent->timer ( + after => 0, # first invoke ASAP + interval => 10, # then invoke every 10 seconds + cb => sub { # the callback to invoke + my @up; + my $str = `upower -i $CONF->{battery}->{upower_device}`; + $charge = '⚡ ??% • 00:00'; + + if ($?) { + return; + } + + @up = split /\n/, $str; + my $percentage; + my $time_to; # full or empty + my $state; + my $hours = 0; + my $minutes = 0; + + while ($str = pop (@up)) { + if (substr ($str, 4, 10) eq 'percentage') { + $percentage = substr $str, 25; + chomp $percentage; + } elsif (substr ($str, 4, 5) eq 'state') { + if (substr ($str, 25, 11) eq 'discharging') { + $state = '▼'; + } elsif (substr ($str, 25, 8) eq 'charging') { + $state = '▲'; + } else { + $state = '•'; + } + } elsif (substr ($str, 4, 13) eq 'time to empty') { + $time_to = substr $str, 25; + } elsif (substr ($str, 4, 12) eq 'time to full') { + $time_to = substr $str, 25; + } + } + + unless (defined $percentage && defined $state) { + return; + } + + if (defined $time_to) { + chomp $time_to; + + if (substr ($time_to, -7) eq 'minutes') { + $minutes = substr $time_to, 0, -8; + $minutes =~ tr/\,/./; + $minutes = int $minutes; + } elsif (substr ($time_to, -5) eq 'hours') { + $hours = substr $time_to, 0, -6; + $hours =~ tr/\,/./; + $minutes = int (60 * ($hours - int ($hours))); + } + + $time_to = sprintf '%02d:%02d', $hours, $minutes; + } else { + $time_to = '00:00'; + } + + $charge = sprintf '⚡ %s %s %s', $percentage, $state, $time_to; + } + ); + } else { + # fallback borrow data from /sys + push @EP, AnyEvent->timer ( + after => 0, # first invoke ASAP + interval => 10, # then invoke every 10 seconds + cb => sub { # the callback to invoke + my $current_charge; + my $charge_max; + my $state; + my ($CH, $CCM, $STH); + + unless (open $CH, '<', $BATTERY_CHARGE_NOW_FILE) { + $charge = '⚡ ??% •'; + return; + } + + $current_charge = <$CH>; + chomp $current_charge; + close $CH; + + unless (open $CCM, '<', $BATTERY_CHARGE_FULL_FILE) { + $charge = '⚡ ??% •'; + return; + } + + $charge_max = <$CCM>; + chomp $charge_max; + close $CCM; + + unless (open $STH, '<', sprintf '%s/status', $CONF->{battery}->{sys_dir}) { + $charge = '⚡ ??% •'; + return; + } + + $state = <$STH>; + chomp $state; + close $STH; + + if ($state eq 'Charging') { + $state = '▲'; + } elsif ($state eq 'Discharging') { + $state = '▼'; + } else { + $state = '•'; + } + + my $capacity = int (100 * ($current_charge / $charge_max)); + $charge = sprintf '⚡ %s%% %s', $capacity, $state; + } + ); + } + } + + # put all fast accessable stats in one callback + push @EP, AnyEvent->timer ( + after => 0, # first invoke ASAP + interval => 3, # then invoke every 3 second + cb => sub { # the callback to invoke + + # Fetch Load Average + if ($CONF->{la}) { + if (defined $LAHANDLE) { + if (tell ($LAHANDLE) == -1) { + open $LAHANDLE, '<', '/proc/loadavg'; + } + } else { + open $LAHANDLE, '<', '/proc/loadavg'; + } + + $la = readline $LAHANDLE; + seek $LAHANDLE, SEEK_SET, 0; + $la = (split / /ms, $la, 2)[0]; + } + + # populate memory statistics + if ($CONF->{mem}) { + if (defined $MEMINFOHANDLE) { + if (tell ($MEMINFOHANDLE) == -1) { + open $MEMINFOHANDLE, '<', '/proc/meminfo'; + } + } else { + open $MEMINFOHANDLE, '<', '/proc/meminfo'; + } + + my @mem; + + while ( ! eof ($MEMINFOHANDLE) ) { + push @mem, readline ($MEMINFOHANDLE); + } + + seek $MEMINFOHANDLE, SEEK_SET, 0; + @mem = map { substr $_, 0, -4 } @mem; + my $memtotal = (split /\s+/xms, $mem[0])[1]; + my $memavail = (split /\s+/xms, $mem[2])[1]; + my $swaptotal = (split /\s+/xms, $mem[14])[1]; + my $swapfree = (split /\s+/xms, $mem[15])[1]; + my $shmem = (split /\s+/xms, $mem[20])[1]; + my $memused = int (($memtotal - $memavail) / ($memtotal / 100)); + + $meminfo = sprintf ( + 'M:%d%% SHM:%dM SW:%dM', + $memused, + int ($shmem / 1024), + int (($swaptotal - $swapfree) / 1024) + ); + } + + # populate cpu temperature info + if ($CONF->{cpu_temp}->{enabled}) { + my $hwmon = 1; + + if (defined $CPUTEMPHANDLE) { + if (tell $CPUTEMPHANDLE == -1) { + if (open $CPUTEMPHANDLE, '<', $CONF->{cpu_temp}->{file}) { + $hwmon = 1; + } else { + $hwmon = 0; + } + } + } else { + unless (open $CPUTEMPHANDLE, '<', $CONF->{cpu_temp}->{file}) { + $hwmon = 0; + }; + } + + if ($hwmon) { + $cputemp = readline $CPUTEMPHANDLE; + seek $CPUTEMPHANDLE, SEEK_SET, 0; + chomp $cputemp; + $cputemp = int $cputemp; + + if ($cputemp > 1000) { + $cputemp = int ($cputemp / 1000); + } + + $cputemp = sprintf '%s°', $cputemp; + } + } + }, + ); + + # check openvpn state-file mtime + if ($CONF->{vpn}->{enabled}) { + push @EP, AnyEvent->timer ( + after => 0, # first invoke ASAP + interval => 3, # then invoke every 3 seconds + cb => sub { # the callback to invoke + my $mtime = (stat $CONF->{vpn}->{statusfile}) [9]; + + if (defined $mtime) { + if ((time () - $mtime) < $CONF->{vpn}->{mtime_threshold}) { + $file_check = 1; + } else { + $file_check = 0; + } + + $file_check_mtime = $mtime; + } else { + $mtime = 0; + $file_check = 0; + } + } + ); + + # Try to tcp connect to server in vpn network + if ($CONF->{vpn}->{tcp_check}->{enabled}) { + push @EP, AnyEvent->timer ( + after => 0, # first invoke ASAP + interval => 5, # then invoke every 5 seconds + cb => sub { # the callback to invoke + if ( + my $sock = IO::Socket->new ( + Domain => AF_INET, + proto => 'tcp', + PeerHost => $CONF->{vpn}->{tcp_check}->{host}, + PeerPort => $CONF->{vpn}->{tcp_check}->{port}, + Type => SOCK_STREAM, + Timeout => $CONF->{vpn}->{tcp_check}->{timeout} + ) + ) { + $sock->shutdown (SHUT_WR); + $sock->close (); + $tcp_check = 1; + } else { + $tcp_check = 0; + } + } + ); + } + } + + # poll pulseaudio volume on first sink in list + if ($CONF->{'simple-volume-pa'}) { + push @EP, AnyEvent->timer ( + after => 0, # first invoke ASAP + interval => 1, # then invoke every 1 second + cb => sub { # the callback to invoke + my $pactl = `LC_ALL=C pactl list sinks`; + + if ($?) { + # we've some error here, so quit further processing + return; + } + + foreach my $str (split /\n/, $pactl) { + if ($str =~ /^\s+Volume\:\s+[a-z|A-Z\\-]+\:\s+\d+\s+\/\s+(\d+)% /xms ) { + $simple_volume_pa = $1; + last; + } + } + } + ); + } + + # poll operstate for given net ifs + if ($CONF->{'net-if'}->{enabled}) { + push @EP, AnyEvent->timer ( + after => 0, # first invoke ASAP + interval => 1, # then invoke every 1 second + cb => sub { # the callback to invoke + my @ifstate; + + foreach my $if (@{$CONF->{'net-if'}->{if}}) { + if (open my $STATEFH, '<', "$if->{dir}/operstate") { + my $state = <$STATEFH>; + close $STATEFH; + chomp $state; + + if ($state eq 'up') { + push @ifstate, sprintf '%s:▲', $if->{name}, $CONF->{'net-if'}->{up_color}; + } else { + push @ifstate, sprintf '%s:▼', $if->{name}, $CONF->{'net-if'}->{down_color}; + } + } + } + + if ($#ifstate >=0 ) { + $interfaces = join ' ', @ifstate; + } + } + ); + } + + # run all cron jobs from config + if ($CONF->{'cron'}->{enabled}) { + foreach my $task (@{$CONF->{'cron'}->{tasks}}) { + push @EP, AnyEvent::Timer::Cron->new ( + time_zone => $CONF->{cron}->{timezone}, + cron => $task->{time}, + cb => sub { + foreach my $cmd (@{$task->{cmd}}) { + `$cmd`; + } + } + ) + } + } + + return; +} + +sub spawn (@) { + my @args = @_; + my $pid = fork (); + + if ($pid) { + # Parent, work done, return + return; + } elsif ($pid == 0) { # Child + setsid (); + # it seems that at least vscode creashes if we close stdio handles + open STDOUT, '>', '/dev/null'; + open STDERR, '>', '/dev/null'; + open STDIN, '<', '/dev/null'; + $pid = fork (); + + if ($pid) { # Parent + # in this place parent quits + exit 0; + } elsif ($pid == 0) { # Child + setsid (); + my $prg = shift @args; + # weird situation, silently quit here :) + exit 0 unless (defined $prg); + + if (@args > 0) { + exec $prg, @args; + } else { + exec $prg; + } + } + # else - we just unable to fork, so silently ignore event + } + + return; +} + +sub find_caps_led () { + my $str; + opendir my $C, '/sys/class/input'; + my $cnt = 0; + + while (readdir $C) { + if (-f sprintf ('/sys/class/input/%s/input0::capslock/brightness', $_)) { + $str = sprintf '/sys/class/input/%s/input0::capslock/brightness', $_; + last; + } + + if (-f sprintf ('/sys/class/input/%s/input%s::capslock/brightness', $_, $cnt)) { + $str = sprintf '/sys/class/input/%s/input%s::capslock/brightness', $_, $cnt; + last; + } + + $cnt++; + } + + closedir $C; + return $str; +} + +sub to_hours ($) { + my $sec = shift; + + my $hrs = int ($sec / (60 * 60)); + my $min = int (($sec - ($hrs * 60 * 60)) / 60); + + return sprintf '%02d:%02d', $hrs, $min; +} + +sub readconf () { + my $i3dir = "$ENV{HOME}/.config/i3"; + + unless (-d $i3dir) { + $i3dir = "$ENV{HOME}/.i3"; + } + + my $file = "$i3dir/i3status-anyevent.json"; + + unless (-f $file) { + $file = "$i3dir/config.json"; + } + + my $FH; + + unless (open $FH, '<', $file) { + die "Unable to open $file: $!\n"; + } + + my $len = (stat $file)[7]; + my $data; + use bytes; + my $readlen = read $FH, $data, $len; + no bytes; + close $FH; + + unless (defined $readlen) { + die "Unable to read $file: $!\n"; + } + + if ($readlen != $len) { + die "Unable to read $file: read $readlen, but stat() reports that file is $len bytes\n"; + } + + my $config = eval { JSON::XS->new->utf8->relaxed->decode ($data) }; + + unless ($config) { + die "Unable to decode $file: $@\n"; + } + + return $config; +} + +sub active_win_borders () { + foreach my $app (@{$CONF->{apps}}) { + unless (defined $app->{border_inactive}) { + $app->{border_inactive} = $app->{border}; + } + + unless (defined $app->{border_active}) { + $app->{border_active} = $app->{border}; + } + + if (defined ($app->{instance}) && defined ($win->{instance}->{$app->{instance}})) { + $app->{border} = $app->{border_active}; + } else { + $app->{border} = $app->{border_inactive}; + } + } + + return; +} diff --git a/i3status-anyevent.json b/i3status-anyevent.json new file mode 100644 index 0000000..67d8071 --- /dev/null +++ b/i3status-anyevent.json @@ -0,0 +1,294 @@ +# Enable "plugin" 1, disable "plugin" 0 +{ +# default text and background colors +"color" : "#3e78fd", +"background" : "#edeceb", + +# Load Average +"la" : 1, + +# Memory stats +"mem" : 1, + +# Just clock +"clock": { + "enabled" : 1, +# html (actually, pango) color notation + "color" : "#000000", + "left_click" : { + "enabled" : 1, + "cmd" : "orage" + }, + "right_click" : { + "enabled" : 0, + "cmd" : "true" + } +}, + +# Battery +"battery": { + "enabled" : 1, +# can be upower or something else to use /sys polling instead of periodic run of upower binary + "driver" : "upower", +# where we can find charge_{now,full} and status files + "sys_dir" : "/sys/class/power_supply/BAT0", +# invoke 'upower --enumerate' and choose one + "upower_device" : "/org/freedesktop/UPower/devices/battery_BAT0" +}, + +# 0-th core temperature +"cpu_temp": { + "enabled": 1, + "file": "/sys/class/hwmon/hwmon0/temp1_input" +}, + +# Caps Lock status +"capslock": { + "enabled" : 1, + "background" : "#edeceb", + "color" : "#3e78fd" +}, + +# OpenVPN status. Note that directive status in openvpn.conf should point to +# state file and update its timestamp at least as frequent as mtime_threshold in seconds +"vpn": { + "enabled": 0, + "statusfile" : "/tmp/openvpn-status.stat", + "mtime_threshold": 10, +# Also we can tcp-ping some service inside vpn network to ensure that connection is not stoned + "tcp_check" : { + "enabled": 0, + "host": "somehost.tld", + "port": 443, + "timeout" : 3 + } +}, + +# Simplified volume indicator +# it periodically polls pulseaudio via "pactl list sinks" command +# so it's not light on system resources as i'd like +"simple-volume-pa": 0, + +"net-if" : { + "enabled" : 0, +# html (actually, pango) notation + "down_color" : "red", + "up_color" : "green", + "if" : [ + { + "name" : "eth0", + "dir" : "/sys/class/net/eth0" + }, + { + "name" : "wlan0", + "dir" : "/sys/class/net/wlan0" + } + ] +}, + +# Intended for use with desktop notifications +# Because it inherits all env vars from i3, it can intercat with dbus and other +# such user session services. +"cron" : { + "enabled" : 0, + "tasks" : [ + { + "time" : "55 23 * * *", + "cmd" : [ + "notify-send 'Sleeping Time!' 'Take a nap.'", + "mpg123 -q /usr/share/sounds/complete_quest_requirement.mp3" + ] + }, + + { +# time in crontab format + "time" : "55 12 * * 2,4", +# commands as it should be invoked in bash script + "cmd" : [ + "notify-send 'Pizza time!' 'M-m-m, tasty!'", + "mpg123 -q /usr/share/sounds/complete_quest_requirement.mp3" + ] + } + ] +}, + +# Weather to display Application Buttons +"app_buttons": 1, + +# Buttons - application launch "buttons" +"apps": [ + { + "full_text" : " X ", + "name" : "kill_fbpanel", + "cmd" : "killall -9 fbpanel", + "color": "#aa0000", + "border": "#adadad", + "border_active": "#3e78fd", + "separator": false, + "separator_block_width": 2 + }, + { + +# text displayed on button + "full_text" : " Term ", + +# internal application name, must be unique + "name" : "lilyterm", + +# actual commad to be performed if button clicked + "cmd" : "lilyterm", + +# WM_CLASS(STRING) = "irssi", "URxvt" +# ^-instance ^-class + +# window instance as reported by xprop + "instance" : "lilyterm", + +# window class as reported by xprop + "class" : "LilyTerm", + +# text color + "color" : "#666666", + +# normal border color + "border": "#adadad", + +# border color if there is at lest one app window on desktop + "border_active": "#3e78fd", + +# if true - '|' will be drawn on panel to the right side of the "button" + "separator": false, + +# amount of space in pixels between buttons + "separator_block_width": 2 + }, + { + "full_text" : " Ff ", + "name" : "firefox", + "cmd" : "firefox", + "instance" : "Navigator", + "class" : "Firefox", + "color" : "#ffa500", + "border": "#adadad", + "border_active": "#3e78fd", + "separator": false, + "separator_block_width": 2 + }, + { + "full_text" : " V ", + "name" : "vivaldi", + "cmd" : "vivaldi", + "instance" : "vivaldi-stable", + "class" : "Vivaldi-stable", + "color" : "#ffa500", + "border": "#adadad", + "border_active": "#3e78fd", + "separator": false, + "separator_block_width": 2 + }, + { + "full_text" : " Chr ", + "name" : "chrome", + "cmd" : "chrome", + "instance" : "google-chrome", + "class" : "Google-chrome", + "color" : "#ffa500", + "border": "#adadad", + "border_active": "#3e78fd", + "separator": false, + "separator_block_width": 2 + }, + { + "full_text" : " O ", + "name" : "opera", + "cmd" : "opera", + "instance" : "Opera", + "class" : "Opera", + "color" : "#ffa500", + "border": "#adadad", + "border_active": "#3e78fd", + "separator": false, + "separator_block_width": 2 + }, + { + "full_text" : " @ ", + "name" : "thunderbird", + "cmd" : "thunderbird", + "instance" : "Mail", + "class" : "Thunderbird", + "color" : "#006994", + "border": "#adadad", + "border_active": "#3e78fd", + "separator": false, + "separator_block_width": 2 + }, + { + "full_text" : " Tg ", + "name" : "telegram", + "cmd" : "Telegram", + "instance" : "Telegram", + "class" : "TelegramDesktop", + "color" : "#5a8850", + "border": "#adadad", + "border_active": "#3e78fd", + "separator": false, + "separator_block_width": 2 + }, + { + "full_text" : " M ", + "name" : "myteam", + "cmd" : "myteam", + "instance" : "myteam", + "class" : "myteam", + "color" : "#5a8850", + "border": "#adadad", + "border_active": "#3e78fd", + "separator": false, + "separator_block_width": 2 + }, + { + "full_text" : " S ", + "name" : "slack", + "cmd" : "slack", + "instance" : "slack", + "class" : "Slack", + "color" : "#5a8850", + "border": "#adadad", + "border_active": "#3e78fd", + "separator": false, + "separator_block_width": 2 + }, + { + "full_text" : " Txt ", + "name" : "mousepad", + "cmd" : "mousepad", + "instance" : "mousepad", + "class" : "Mousepad", + "border": "#adadad", + "border_active": "#3e78fd", + "separator": false, + "separator_block_width": 2 + }, + { + "full_text" : " VSC ", + "name" : "code", + "cmd" : "code", + "instance" : "code", + "class" : "Code", + "border": "#adadad", + "border_active": "#3e78fd", + "separator": false, + "separator_block_width": 2 + }, + { + "full_text" : " K ", + "name" : "keepassxc", + "cmd" : "keepassxc", + "instance" : "keepassxc", + "class" : "KeePassXC", + "color" : "#7ec850", + "border" : "#adadad", + "border_active" : "#3e78fd" + } + ] +}