0
Files
i3status-ae/i3status-anyevent
Sergei Fedosov 91cf117ee8 Do not call AnyEvent->now_update every 2 minutes, as it looks useless in this script
We reload all event hendlers if time skew in script own counter more
than 60 seconds as per ab68a26e84, no
other timekeeping issues are reported, so considering
AnyEvent->now_update useless for this particular usecase.
2021-07-20 12:07:48 +03:00

943 lines
24 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::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 ();
# load AnyEvent::Timer::Cron only if cron enabled in config
if ($CONF->{'cron'}->{enabled}) {
require AnyEvent::Timer::Cron;
}
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 ();
my $CAPS_LED_SKIPS = 0;
my $CAPS_LED_SKIPS_MAX = 180;
# 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
# 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;
my $xsetflag = 0;
if (defined $CAPS_LED_PATH) {
unless (open $CAPSHANDLE, '<', $CAPS_LED_PATH) {
$CAPS_LED_PATH = undef;
$caps_state = 2
}
} else {
# we can use "$ xset q | grep Caps" as fallback, but it is notably more expensive check
my $xset = `xset q`;
$xsetflag = 1;
if ($?) {
# some kind of error, so we assume that we cannot update caps lock status
$caps_state = 2;
} else {
$xset =~ /Caps Lock\:\s+(o[n|f]f?)\s+/;
if ($1 eq 'off') {
$caps_state = 0;
} elsif ($1 eq 'on') {
$caps_state = 1;
} else {
$caps_state = 2;
}
}
if ($CAPS_LED_SKIPS == $CAPS_LED_SKIPS_MAX) {
$CAPS_LED_SKIPS = 0;
$CAPS_LED_PATH = find_caps_led ();
} else {
$CAPS_LED_SKIPS++;
}
}
if ($caps_state ne '2') {
unless ($xsetflag) {
$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'}->{enabled}) {
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 ) {
my $volumestr = sprintf '<big>%s</big> %s%%', $CONF->{'simple-volume-pa'}->{symbol}, $1;
if (! defined $O->{'simple-volume-pa'}->{'full_text'}) {
$O->{'simple-volume-pa'}->{'full_text'} = $volumestr;
$O->{'simple-volume-pa'}->{'color'} = '#3e78fd';
$O->{'simple-volume-pa'}->{'markup'} = 'pango';
$UPDATE = 1;
} elsif ($O->{'simple-volume-pa'}->{'full_text'} ne $volumestr) {
$O->{'simple-volume-pa'}->{'full_text'} = $volumestr;
$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 crashes 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;
}