AnyEvent does not sync its timers with system time, so cron jobs that scheduled for particular time, shoots with time skew after wakeup from hibernate or suspend. And AnyEvent have no mechanism to sync its timers with local system time. The only way to "sync" timers is to remove and re-add watchers, so timers begin count time from beginning. To workaround timekeeping issue for AnyEvent::Timer::Cron let's add another time-based call back that will send SIGHUP to script if local system time differs from time counter more than 60 seconds. SIGHUP should trigger signal handler that remove all event watchers, re-read config and re-add event watchers effectively reloads config and re-launch timers for AnyEvent.
913 lines
23 KiB
Perl
913 lines
23 KiB
Perl
#!/usr/bin/perl
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
# 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';
|
|
|
|
# Local Time Counter
|
|
my $TC = time();
|
|
|
|
# event pool
|
|
my @EP;
|
|
|
|
# STDOUT spew flag variable
|
|
my $UPDATE = 0;
|
|
|
|
# output json "object" that used for STDOUT json generation
|
|
my $O = undef;
|
|
|
|
# reload config and re-apply it on SIGHUP
|
|
local $SIG{HUP} = sub {
|
|
$O = undef;
|
|
$CONF = readconf ();
|
|
active_win_borders ();
|
|
$#EP = -1;
|
|
add_events ();
|
|
};
|
|
|
|
my @month = ('Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек');
|
|
my @week_day = ('Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб');
|
|
|
|
# everything for text labels on i3bar
|
|
my $file_check_mtime = 0;
|
|
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 {*STDOUT} << 'EOJSON';
|
|
{"version": 1, "click_events": true}
|
|
[ [],
|
|
EOJSON
|
|
|
|
# i3 events should persist over config reloads
|
|
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 ();
|
|
$UPDATE = 1;
|
|
}
|
|
};
|
|
|
|
$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 () {
|
|
# Create stdout json "object" stub from static objects
|
|
if ($CONF->{app_buttons}) {
|
|
@{$O->{apps}} = @{$CONF->{apps}};
|
|
}
|
|
|
|
# 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 {
|
|
# Print JSON only if there are updates ready
|
|
if ($UPDATE) {
|
|
my $jsonobj;
|
|
|
|
if ($CONF->{app_buttons}) {
|
|
@{$jsonobj} = @{$O->{apps}};
|
|
}
|
|
|
|
if ($CONF->{capslock}->{enabled}) {
|
|
push @{$jsonobj}, $O->{capslock};
|
|
}
|
|
|
|
if ($CONF->{cpu_temp}->{enabled}) {
|
|
push @{$jsonobj}, $O->{cputemp};
|
|
}
|
|
|
|
if ($CONF->{mem}) {
|
|
push @{$jsonobj}, $O->{mem};
|
|
}
|
|
|
|
if ($CONF->{la}) {
|
|
push @{$jsonobj}, $O->{la};
|
|
}
|
|
|
|
if ($CONF->{vpn}->{enabled}) {
|
|
push @{$jsonobj}, $O->{vpn};
|
|
}
|
|
|
|
if ($CONF->{'net-if'}->{enabled}) {
|
|
push @{$jsonobj}, $O->{interfaces};
|
|
}
|
|
|
|
if ($CONF->{battery}->{enabled}) {
|
|
push @{$jsonobj}, $O->{battery};
|
|
}
|
|
|
|
if ($CONF->{'simple-volume-pa'}) {
|
|
push @{$jsonobj}, $O->{'simple-volume-pa'};
|
|
}
|
|
|
|
if ($CONF->{clock}->{enabled}) {
|
|
push @{$jsonobj}, $O->{wallclock};
|
|
}
|
|
|
|
my $output_string = eval { encode_json $jsonobj };
|
|
|
|
if (defined $output_string) {
|
|
print {*STDOUT} decode ('UTF-8', $output_string) . ",\n";
|
|
} else {
|
|
warn $@;
|
|
}
|
|
|
|
$UPDATE = 0;
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
# 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
|
|
my $caps_state = 0;
|
|
my $CAPSHANDLE;
|
|
|
|
if (defined $CAPS_LED_PATH) {
|
|
unless (open $CAPSHANDLE, '<', $CAPS_LED_PATH) {
|
|
$CAPS_LED_PATH = undef;
|
|
$caps_state = 2
|
|
}
|
|
} else {
|
|
$caps_state = 2;
|
|
}
|
|
|
|
if ($caps_state ne '2') {
|
|
$caps_state = readline $CAPSHANDLE;
|
|
close $CAPSHANDLE;
|
|
chomp $caps_state;
|
|
|
|
if (! defined $O->{capslock}->{state}) {
|
|
$O->{capslock}->{border} = $CONF->{capslock}->{background};
|
|
$O->{capslock}->{full_text} = 'caps';
|
|
$O->{capslock}->{markup} = 'pango';
|
|
$O->{capslock}->{state} = $caps_state;
|
|
|
|
if ($caps_state) {
|
|
$O->{capslock}->{background} = $CONF->{capslock}->{color};
|
|
$O->{capslock}->{color} = $CONF->{capslock}->{background};
|
|
} else {
|
|
$O->{capslock}->{background} = $CONF->{capslock}->{background};
|
|
$O->{capslock}->{color} = $CONF->{capslock}->{color};
|
|
}
|
|
|
|
$UPDATE = 1;
|
|
} elsif ($O->{capslock}->{state} ne $caps_state) {
|
|
$O->{capslock}->{state} = $caps_state;
|
|
|
|
if ($caps_state) {
|
|
$O->{capslock}->{background} = $CONF->{capslock}->{color};
|
|
$O->{capslock}->{color} = $CONF->{capslock}->{background};
|
|
} else {
|
|
$O->{capslock}->{background} = $CONF->{capslock}->{background};
|
|
$O->{capslock}->{color} = $CONF->{capslock}->{color};
|
|
}
|
|
|
|
$UPDATE = 1;
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
# 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;
|
|
my $wallclock = sprintf '<big> %s, %s %s %s %02s:%02s </big>', $week_day[$wday], $mday, $month[$mon], $year, $hour, $min;
|
|
|
|
if (! defined $O->{wallclock}->{'full_text'}) {
|
|
$O->{wallclock}->{'full_text'} = $wallclock;
|
|
$O->{wallclock}->{'name'} = 'wallclock';
|
|
$O->{wallclock}->{'color'} = '#666666';
|
|
$O->{wallclock}->{'markup'} = 'pango';
|
|
$UPDATE = 1;
|
|
} elsif ($O->{wallclock}->{'full_text'} ne $wallclock) {
|
|
$O->{wallclock}->{'full_text'} = $wallclock;
|
|
$UPDATE = 1;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
# 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}`;
|
|
|
|
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, -1; # chop % symbol ath the end of the string
|
|
} 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;
|
|
}
|
|
|
|
my $color = '<span foreground="Blue">';
|
|
|
|
if ($percentage > 85) {
|
|
$color = '<span foreground="LimeGreen">';
|
|
} elsif ($percentage < 25 && $percentage > 15) {
|
|
$color = '<span foreground="Goldenrod">';
|
|
} elsif ($percentage <= 15 && $percentage > 11) {
|
|
$color = '<span foreground="DarkOrange">';
|
|
} elsif ($percentage <= 11) {
|
|
$color = '<span foreground="Red">';
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
my $charge = sprintf '<b><big><span foreground="Goldenrod">⚡</span></big></b> %s%s%%</span> %s %s', $color, $percentage, $state, $time_to;
|
|
|
|
if (! defined $O->{battery}->{'full_text'}) {
|
|
$O->{battery}->{'full_text'} = $charge;
|
|
$O->{battery}->{'color'} = '#3e78fd';
|
|
$O->{battery}->{'markup'} = 'pango';
|
|
$UPDATE = 1;
|
|
} elsif ($O->{battery}->{'full_text'} ne $charge) {
|
|
$O->{battery}->{'full_text'} = $charge;
|
|
$UPDATE = 1;
|
|
}
|
|
}
|
|
);
|
|
} 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);
|
|
|
|
if (!defined $O->{battery}->{'full_text'}) {
|
|
$O->{battery}->{'full_text'} = '<big>⚡</big> ??% •';
|
|
$O->{battery}->{'color'} = '#3e78fd';
|
|
$O->{battery}->{'markup'} = 'pango';
|
|
}
|
|
|
|
unless (open $CH, '<', $BATTERY_CHARGE_NOW_FILE) {
|
|
if ($O->{battery}->{'full_text'} ne '<big>⚡</big> ??% •') {
|
|
$O->{battery}->{'full_text'} = '<big>⚡</big> ??% •';
|
|
$UPDATE = 1;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$current_charge = <$CH>;
|
|
chomp $current_charge;
|
|
close $CH;
|
|
|
|
unless (open $CCM, '<', $BATTERY_CHARGE_FULL_FILE) {
|
|
if ($O->{battery}->{'full_text'} ne '<big>⚡</big> ??% •') {
|
|
$O->{battery}->{'full_text'} = '<big>⚡</big> ??% •';
|
|
$UPDATE = 1;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$charge_max = <$CCM>;
|
|
chomp $charge_max;
|
|
close $CCM;
|
|
|
|
unless (open $STH, '<', sprintf '%s/status', $CONF->{battery}->{sys_dir}) {
|
|
if ($O->{battery}->{'full_text'} ne '<big>⚡</big> ??% •') {
|
|
$O->{battery}->{'full_text'} = '<big>⚡</big> ??% •';
|
|
$UPDATE = 1;
|
|
}
|
|
|
|
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));
|
|
my $color = '<span foreground="Blue">';
|
|
|
|
if ($capacity > 85) {
|
|
$color = '<span foreground="LimeGreen">';
|
|
} elsif ($capacity < 25 && $capacity > 15) {
|
|
$color = '<span foreground="Goldenrod">';
|
|
} elsif ($capacity <= 15 && $capacity > 11) {
|
|
$color = '<span foreground="DarkOrange">';
|
|
} elsif ($capacity <= 11) {
|
|
$color = '<span foreground="Red">';
|
|
}
|
|
|
|
my $charge = sprintf '<b><big><span foreground="Goldenrod">⚡</span></big></b> %s%s%%</span> %s', $color, $capacity, $state;
|
|
|
|
if ($O->{battery}->{'full_text'} ne $charge) {
|
|
$O->{battery}->{'full_text'} = $charge;
|
|
$UPDATE = 1;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
# 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}) {
|
|
open my $LAHANDLE, '<', '/proc/loadavg'; ## no critic (InputOutput::RequireCheckedOpen)
|
|
my $la = readline $LAHANDLE;
|
|
close $LAHANDLE;
|
|
$la = sprintf 'LA: %s', (split / /ms, $la, 2)[0];
|
|
|
|
if (! defined $O->{la}->{'full_text'}) {
|
|
$O->{la}->{'full_text'} = $la;
|
|
$O->{la}->{'color'} = '#3e78fd';
|
|
$UPDATE = 1;
|
|
} elsif ($O->{la}->{'full_text'} ne $la) {
|
|
$O->{la}->{'full_text'} = $la;
|
|
$UPDATE = 1;
|
|
}
|
|
}
|
|
|
|
# populate memory statistics
|
|
if ($CONF->{mem}) {
|
|
open my $MEMINFOHANDLE, '<', '/proc/meminfo'; ## no critic (InputOutput::RequireCheckedOpen)
|
|
my @mem;
|
|
|
|
while ( ! eof ($MEMINFOHANDLE) ) {
|
|
push @mem, readline ($MEMINFOHANDLE);
|
|
}
|
|
|
|
close $MEMINFOHANDLE;
|
|
@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));
|
|
|
|
my $mem = sprintf (
|
|
'M:%d%% SHM:%dM SW:%dM',
|
|
$memused,
|
|
int ($shmem / 1024),
|
|
int (($swaptotal - $swapfree) / 1024)
|
|
);
|
|
|
|
if (! defined $O->{mem}->{'full_text'}) {
|
|
$O->{mem}->{'full_text'} = $mem;
|
|
$O->{mem}->{'color'} = '#3e78fd';
|
|
$UPDATE = 1;
|
|
} elsif ($O->{mem}->{'full_text'} ne $mem) {
|
|
$O->{mem}->{'full_text'} = $mem;
|
|
$UPDATE = 1;
|
|
}
|
|
}
|
|
|
|
# populate cpu temperature info
|
|
if ($CONF->{cpu_temp}->{enabled}) {
|
|
my $hwmon = 1;
|
|
my $CPUTEMPHANDLE;
|
|
|
|
unless (open $CPUTEMPHANDLE, '<', $CONF->{cpu_temp}->{file}) {
|
|
$hwmon = 0;
|
|
};
|
|
|
|
if ($hwmon) {
|
|
my $cputemp = readline $CPUTEMPHANDLE;
|
|
close $CPUTEMPHANDLE;
|
|
chomp $cputemp;
|
|
$cputemp = int $cputemp;
|
|
|
|
if ($cputemp > 1000) {
|
|
$cputemp = int ($cputemp / 1000);
|
|
}
|
|
|
|
$cputemp = sprintf '%s°', $cputemp;
|
|
|
|
if (! defined $O->{cputemp}->{'full_text'}) {
|
|
$O->{cputemp}->{'full_text'} = $cputemp;
|
|
$O->{cputemp}->{'color'} = '#3e78fd';
|
|
$UPDATE = 1;
|
|
} elsif ($O->{cputemp}->{'full_text'} ne $cputemp) {
|
|
$O->{cputemp}->{'full_text'} = $cputemp;
|
|
$UPDATE = 1;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
# 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];
|
|
my $vpnstr = 'VPN:';
|
|
|
|
if (defined $mtime) {
|
|
if ((time () - $mtime) < $CONF->{vpn}->{mtime_threshold}) {
|
|
$vpnstr .= sprintf '<span foreground="%s">⍋</span>', $CONF->{vpn}->{up_color};
|
|
} else {
|
|
$vpnstr .= sprintf '<span foreground="%s">⍒</span>', $CONF->{vpn}->{down_color};
|
|
}
|
|
|
|
$file_check_mtime = $mtime;
|
|
} else {
|
|
$mtime = 0;
|
|
$vpnstr .= sprintf '<span foreground="%s">⍒</span>', $CONF->{vpn}->{up_down};
|
|
}
|
|
|
|
# Try to tcp connect to server in vpn network
|
|
if ($CONF->{vpn}->{tcp_check}->{enabled}) {
|
|
$vpnstr .= ':';
|
|
|
|
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 ();
|
|
$vpnstr .= sprintf '<span foreground="%s">✔</span>', $CONF->{vpn}->{up_color};
|
|
} else {
|
|
$vpnstr .= sprintf '<span foreground="%s">✘</span>', $CONF->{vpn}->{down_color};
|
|
}
|
|
}
|
|
|
|
if (! defined $O->{vpn}->{'full_text'}) {
|
|
$O->{vpn}->{'markup'} = 'pango';
|
|
$O->{vpn}->{'full_text'} = $vpnstr;
|
|
$O->{vpn}->{'color'} = '#3e78fd';
|
|
$UPDATE = 1;
|
|
} elsif ($O->{vpn}->{'full_text'} ne $vpnstr) {
|
|
$O->{vpn}->{'full_text'} = $vpnstr;
|
|
$UPDATE = 1;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
# 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 ) {
|
|
if (! defined $O->{'simple-volume-pa'}->{'full_text'}) {
|
|
$O->{'simple-volume-pa'}->{'full_text'} = $1;
|
|
$O->{'simple-volume-pa'}->{'color'} = '#3e78fd';
|
|
$UPDATE = 1;
|
|
} elsif ($O->{'simple-volume-pa'}->{'full_text'} ne $1) {
|
|
$O->{'simple-volume-pa'}->{'full_text'} = $1;
|
|
$UPDATE = 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;
|
|
my $interfaces;
|
|
|
|
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:<span foreground="%s">▲</span>', $if->{name}, $CONF->{'net-if'}->{up_color};
|
|
} else {
|
|
push @ifstate, sprintf '%s:<span foreground="%s">▼</span>', $if->{name}, $CONF->{'net-if'}->{down_color};
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($#ifstate >=0 ) {
|
|
$interfaces = join ' ', @ifstate;
|
|
}
|
|
|
|
if (! defined $O->{interfaces}->{full_text}) {
|
|
$O->{interfaces}->{color} = $CONF->{color};
|
|
$O->{interfaces}->{full_text} = $interfaces;
|
|
$O->{interfaces}->{markup} = 'pango';
|
|
$UPDATE = 1;
|
|
} elsif ($O->{interfaces}->{full_text} ne $interfaces) {
|
|
$O->{interfaces}->{full_text} = $interfaces;
|
|
$UPDATE = 1;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
# 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`;
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
# also we need workaround for timers after sleep or hibernate
|
|
push @EP, AnyEvent->timer (
|
|
after => 60, # first invoke after 1 minute
|
|
interval => 60, # then invoke every 1 second
|
|
cb => sub { # the callback to invoke
|
|
$TC = $TC + 60;
|
|
my $timeskew = $TC - time ();
|
|
|
|
if (($timeskew > 60) || ($timeskew < -60)) {
|
|
kill 'HUP', $$;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
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'; ## no critic (InputOutput::RequireCheckedOpen)
|
|
open STDERR, '>', '/dev/null'; ## no critic (InputOutput::RequireCheckedOpen)
|
|
open STDIN, '<', '/dev/null'; ## no critic (InputOutput::RequireCheckedOpen)
|
|
$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;
|
|
}
|