0

Compare commits

...

17 Commits

Author SHA1 Message Date
eleksir
7a8316992a Trigger "button" boreder repaint only on new/closed window event 2023-05-08 19:30:55 +03:00
eleksir
5977c933a7 Import missing function 2022-04-30 01:32:15 +03:00
eleksir
d0f7426003 A bit more careful about substr 2022-04-30 01:24:23 +03:00
eleksir
d9719b1ddd Import only what we require 2022-04-30 01:07:15 +03:00
eleksir
2a588e5ae9 Fix 100% cpu usage when spawn nonexisting program 2022-04-30 01:04:19 +03:00
eleksir
2a33b7a4f3 Add .gitignore and make use of English 2021-10-31 21:04:53 +03:00
Sergei Fedosov
1f4e242253 Fix vpn status color when vpn not yet change status 2021-07-26 15:30:36 +03:00
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
Sergei Fedosov
371dc68667 Revert "Read all supplied lines from STDIN, not just first one."
This reverts commit 08bf20d43a. Do not
read whole STDIN as it somehow breaks whidow tracking feature.
2021-07-20 12:04:19 +03:00
Sergei Fedosov
08bf20d43a Read all supplied lines from STDIN, not just first one. 2021-07-20 01:58:15 +03:00
eleksir
24b7a97372 Fix assorted typos. 2021-07-19 23:16:25 +03:00
Sergei Fedosov
c634ca1f40 Use "xset q" as fallback for detecting capslock status if hardware keyboard is missing 2021-07-19 21:26:04 +03:00
Sergei Fedosov
fba1d3dc5d Use pictogram from config for simple-volume-pa plugin 2021-07-19 18:40:45 +03:00
Sergei Fedosov
f63dba554c use AnyEvent::Timer::Cron only if cron enabled in config 2021-07-19 17:26:43 +03:00
Sergei Fedosov
ab68a26e84 Sync AnyEvent timer (brutal way) if timeskew more than 60 seconds.
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.
2021-07-19 14:42:30 +03:00
Sergei Fedosov
b801bf0a76 Color up upower battery status and vpn status 2021-07-19 14:07:50 +03:00
eleksir
9851c59f48 Add perlcritic, generate json output only on update
* Use perlcritic to lint script, add .perlcriticrc config
* Do not generate json on every output iteration, use $UPDATE variable
  to detect if updates are ready.
2021-07-18 18:59:58 +03:00
6 changed files with 368 additions and 241 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.idea/**
.vscode/**

27
.perlcriticrc Normal file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env perlcritic
severity = 5
verbose = 8
[-ControlStructures::ProhibitUnlessBlocks]
[-Variables::RequireLocalizedPunctuationVars]
[-Subroutines::ProhibitSubroutinePrototypes]
[-ErrorHandling::RequireCarping]
[-ValuesAndExpressions::ProhibitEmptyQuotes]
[-ValuesAndExpressions::ProhibitMagicNumbers]
[-ValuesAndExpressions::ProhibitNoisyQuotes]
[-Subroutines::ProhibitExcessComplexity]
[-InputOutput::ProhibitBacktickOperators]
[-RegularExpressions::RequireExtendedFormatting]
[-RegularExpressions::ProhibitEnumeratedClasses]
[-RegularExpressions::RequireLineBoundaryMatching]
[-RegularExpressions::RequireDotMatchAnything]
[-ControlStructures::ProhibitCascadingIfElse]
[-InputOutput::RequireCheckedSyscalls]
[-InputOutput::RequireCheckedClose]
[-InputOutput::RequireBriefOpen]
[-CodeLayout::ProhibitParensWithBuiltins]
[-ControlStructures::ProhibitPostfixControls]
[-ValuesAndExpressions::ProhibitLongChainsOfMethodCalls]
[-Modules::RequireVersionVar]
[-CodeLayout::RequireTidyCode]
[-TestingAndDebugging::RequireUseWarnings]

View File

@@ -17,15 +17,15 @@
* Температуру процессора (одно ядро, указывается файл-источник сведений)
* Состояние клавиши CapsLock
* Состояние одного интерфейса VPN (+ tcp health check, т.к. ovpn, например, умеет "залипать")
* Уровень звука согласно pulse audio для default sink (придумывать "крутилочку" для звука я не стал, ибо там не всё так просто и "в процессе" fun скорее всего пропадёт.). Предполагается, что звук правляется через что-то ещё, например, через какой-нибудь pasystray, например.
* Уровень звука согласно pulse audio для default sink (придумывать "крутилочку" для звука я не стал, ибо там не всё так просто и "в процессе" fun скорее всего пропадёт). Предполагается, что звук управляется через что-то ещё, например, через какой-нибудь pasystray.
* Состояние (up|down) любого набора существующих сетевых интерфейсов. Но каждый надо описать. IP не показываются, т.к. это сжирает место на панели, а его не так-то и много.
* "Кнопки" для запуска произвольных приложений. Собственно, ради этого всё и затевалось :)
* "Трекать" запуск приложенй "с кнопки" и подсвечивать соответствующую "кнопку". Работает через ивенты от самого i3: на появление и на исчезновения окна с рядом свойств.
* Cron! Как ни забавно это звучит, но, да. И нужен он для показывания desktop notifications, ибо системный cron не прокидывает в environment переменную с текущим dbus session. Но в этот крон можно пихнуть что угодно.
* "Трекать" запуск приложений "с кнопки" и подсвечивать соответствующую "кнопку". Работает через ивенты от самого i3: на появление и на исчезновения окна с рядом свойств.
* Cron! Как ни забавно это звучит, но, да. И нужен он для показывания desktop notifications, ибо системный cron не прокидывает в environment переменную с текущим dbus session. Но в этот крон можно пихнуть что угодно, т.к. он запукается уже после старта dbus.
## Как это заставить работать
Скрипт на perl-е, и чтобы заставить его работать нужен, собственно, сам perl, [cpanm][2], [carton][3], [cpm][4] или что-то ещё для управления перловыми модулями, а также крайне желательно наличие в системе библиотеки [lib::local][5].
Скрипт на perl-е, и, чтобы заставить его работать, нужен, собственно, сам perl, [cpanm][2], [carton][3], [cpm][4] или что-то ещё для управления перловыми модулями, а также крайне желательно наличие в системе библиотеки [lib::local][5].
При запуске необходимо убедиться, что переменная [PERL5LIB][6] установлена корректно.

8
TODO
View File

@@ -1,11 +1,3 @@
Что желательно сделать ещё.
* При рестарте i3wm (рестарте скрипта), восстанавливать список запущенных окон.
* Добавить в battery plugin отслеживание уровня заряда. Раскрашивать вывод.
Опционально звать suspend2disk, при очень низком заряде
* Добавить "крутилку" в плагин, отображающий громкость :)
* Применить Use English и использовать человекочитаемые имена встроенных
переменных.

View File

@@ -1,10 +1,15 @@
#!/usr/bin/perl
use strict;
use warnings;
use English qw ( -no_match_vars );
# 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 );
$ENV{PERL_ANYEVENT_MODEL} = 'EV';
exec ( $EXECUTABLE_NAME, $PROGRAM_NAME, @ARGV );
exit 0; # we actually never get here
}
}
@@ -15,15 +20,14 @@ 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 AnyEvent ();
use AnyEvent::I3 qw (i3);
use JSON::XS qw (encode_json decode_json);
use Encode qw (decode);
use IO::Socket qw (AF_INET SOCK_STREAM SHUT_WR);
use POSIX qw (setsid);
local $| = 1;
local $OUTPUT_AUTOFLUSH = 1;
sub add_events ();
sub spawn (@);
@@ -33,15 +37,31 @@ 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;
@@ -51,26 +71,8 @@ local $SIG{HUP} = sub {
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 = '<big>♪</big> ??%';
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};
@@ -89,17 +91,19 @@ unless (-f $BATTERY_CHARGE_FULL_FILE) {
}
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 << 'EOJSON';
print {*STDOUT} << 'EOJSON';
{"version": 1, "click_events": true}
[ [],
EOJSON
# i3 events should persist over config relads
# i3 events should persist over config reloads
my $i3_subscription->{window} = sub {
my $w = shift;
@@ -122,9 +126,13 @@ my $i3_subscription->{window} = sub {
}
}
}
} else {
# we just don't care about other events now
return;
}
active_win_borders ();
$UPDATE = 1;
}
};
@@ -133,7 +141,7 @@ $i3_subscription->{_error} = sub {
die "Connection to i3 closed: $msg";
};
$i3->subscribe($i3_subscription)->recv->{success} || die 'Unable to subscribe to i3 events';
$i3->subscribe ($i3_subscription)->recv->{success} || die 'Unable to subscribe to i3 events';
# now we can add other events
add_events ();
@@ -144,116 +152,43 @@ 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 { # the callback to invoke
my $OUTPUT;
cb => sub {
# Print JSON only if there are updates ready
if ($UPDATE) {
my $jsonobj;
if ($CONF->{app_buttons}) {
@{$OUTPUT} = @{$CONF->{apps}};
}
@{$jsonobj} = @{$O->{apps}} if $CONF->{app_buttons};
push @{$jsonobj}, $O->{capslock} if $CONF->{capslock}->{enabled};
push @{$jsonobj}, $O->{cputemp} if $CONF->{cpu_temp}->{enabled};
push @{$jsonobj}, $O->{mem} if $CONF->{mem};
push @{$jsonobj}, $O->{la} if $CONF->{la};
push @{$jsonobj}, $O->{vpn} if $CONF->{vpn}->{enabled};
push @{$jsonobj}, $O->{interfaces} if $CONF->{'net-if'}->{enabled};
push @{$jsonobj}, $O->{battery} if $CONF->{battery}->{enabled};
push @{$jsonobj}, $O->{'simple-volume-pa'} if $CONF->{'simple-volume-pa'};
push @{$jsonobj}, $O->{wallclock} if $CONF->{clock}->{enabled};
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 ('<big>♪</big> %s%%', $simple_volume_pa),
markup => 'pango'
};
}
if ($CONF->{clock}->{enabled}) {
push @{$OUTPUT}, { name => 'wallclock', color => $CONF->{clock}->{color}, full_text => "<big>$wallclock</big>", markup => 'pango'};
}
$output_string = eval { encode_json $OUTPUT };
my $output_string = eval { encode_json $jsonobj };
if (defined $output_string) {
if ($output_string ne $old_output_string) {
print decode ('UTF-8', $output_string) . ",\n";
$old_output_string = $output_string;
}
print {*STDOUT} decode ('UTF-8', $output_string) . ",\n";
} else {
warn $@;
warn $EVAL_ERROR;
}
@{$OUTPUT} = -1;
$UPDATE = 0;
}
}
);
@@ -272,8 +207,8 @@ sub add_events () {
if ($msg) {
# drop leading ',' if present
if (substr ($msg, 0, 1) eq ',') {
$msg = substr $msg, 1;
if (substr ($msg, 0, length (',')) eq ',') {
$msg = substr $msg, length (',');
}
my $j = eval { decode_json $msg };
@@ -319,29 +254,79 @@ sub add_events () {
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) {
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 {
# we can use "$ xset q | grep Caps" as fallback, but it is notably more expensive check
my $xset = `xset q`;
$xsetflag = 1;
if ($CHILD_ERROR) {
# 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') {
$caps_state = eval { readline ($CAPSHANDLE) };
seek $CAPSHANDLE, SEEK_SET, 0;
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;
}
}
}
);
}
@@ -355,7 +340,18 @@ sub add_events () {
# 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;
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;
}
}
);
}
@@ -370,9 +366,8 @@ sub add_events () {
cb => sub { # the callback to invoke
my @up;
my $str = `upower -i $CONF->{battery}->{upower_device}`;
$charge = '<big>⚡</big> ??% • 00:00';
if ($?) {
if ($CHILD_ERROR) {
return;
}
@@ -384,20 +379,19 @@ sub add_events () {
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') {
if (substr ($str, 4, length ('percentage')) eq 'percentage') {
$percentage = substr $str, 25, 0 - length ('%'); # chop % symbol at the end of the string
} elsif (substr ($str, 4, length ('state')) eq 'state') {
if (substr ($str, 25, length ('discharging')) eq 'discharging') {
$state = '▼';
} elsif (substr ($str, 25, 8) eq 'charging') {
} elsif (substr ($str, 25, length ('charging')) eq 'charging') {
$state = '▲';
} else {
$state = '•';
}
} elsif (substr ($str, 4, 13) eq 'time to empty') {
} elsif (substr ($str, 4, length ('time to empty')) eq 'time to empty') {
$time_to = substr $str, 25;
} elsif (substr ($str, 4, 12) eq 'time to full') {
} elsif (substr ($str, 4, length ('time to full')) eq 'time to full') {
$time_to = substr $str, 25;
}
}
@@ -406,15 +400,27 @@ sub add_events () {
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;
if (substr ($time_to, 0 - length ('minutes')) eq 'minutes') {
$minutes = substr $time_to, 0, 0 - length (' minutes');
$minutes =~ tr/\,/./;
$minutes = int $minutes;
} elsif (substr ($time_to, -5) eq 'hours') {
$hours = substr $time_to, 0, -6;
} elsif (substr ($time_to, 0 - length ('hours')) eq 'hours') {
$hours = substr $time_to, 0, 0 - length (' hours');
$hours =~ tr/\,/./;
$minutes = int (60 * ($hours - int ($hours)));
}
@@ -424,7 +430,17 @@ sub add_events () {
$time_to = '00:00';
}
$charge = sprintf '<big>⚡</big> %s %s %s', $percentage, $state, $time_to;
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 {
@@ -438,8 +454,18 @@ sub add_events () {
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) {
$charge = '<big>⚡</big> ??% •';
if ($O->{battery}->{'full_text'} ne '<big>⚡</big> ??% •') {
$O->{battery}->{'full_text'} = '<big>⚡</big> ??% •';
$UPDATE = 1;
}
return;
}
@@ -448,7 +474,11 @@ sub add_events () {
close $CH;
unless (open $CCM, '<', $BATTERY_CHARGE_FULL_FILE) {
$charge = '<big>⚡</big> ??% •';
if ($O->{battery}->{'full_text'} ne '<big>⚡</big> ??% •') {
$O->{battery}->{'full_text'} = '<big>⚡</big> ??% •';
$UPDATE = 1;
}
return;
}
@@ -457,7 +487,11 @@ sub add_events () {
close $CCM;
unless (open $STH, '<', sprintf '%s/status', $CONF->{battery}->{sys_dir}) {
$charge = '<big>⚡</big> ??% •';
if ($O->{battery}->{'full_text'} ne '<big>⚡</big> ??% •') {
$O->{battery}->{'full_text'} = '<big>⚡</big> ??% •';
$UPDATE = 1;
}
return;
}
@@ -474,7 +508,24 @@ sub add_events () {
}
my $capacity = int (100 * ($current_charge / $charge_max));
$charge = sprintf '<big>⚡</big> %s%% %s', $capacity, $state;
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;
}
}
);
}
@@ -488,37 +539,32 @@ sub add_events () {
# Fetch Load Average
if ($CONF->{la}) {
if (defined $LAHANDLE) {
if (tell ($LAHANDLE) == -1) {
open $LAHANDLE, '<', '/proc/loadavg';
}
} else {
open $LAHANDLE, '<', '/proc/loadavg';
}
open my $LAHANDLE, '<', '/proc/loadavg'; ## no critic (InputOutput::RequireCheckedOpen)
my $la = readline $LAHANDLE;
close $LAHANDLE;
$la = sprintf 'LA: %s', (split / /ms, $la, 2)[0];
$la = readline $LAHANDLE;
seek $LAHANDLE, SEEK_SET, 0;
$la = (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}) {
if (defined $MEMINFOHANDLE) {
if (tell ($MEMINFOHANDLE) == -1) {
open $MEMINFOHANDLE, '<', '/proc/meminfo';
}
} else {
open $MEMINFOHANDLE, '<', '/proc/meminfo';
}
open my $MEMINFOHANDLE, '<', '/proc/meminfo'; ## no critic (InputOutput::RequireCheckedOpen)
my @mem;
while ( ! eof ($MEMINFOHANDLE) ) {
push @mem, readline ($MEMINFOHANDLE);
}
seek $MEMINFOHANDLE, SEEK_SET, 0;
@mem = map { substr $_, 0, -4 } @mem;
close $MEMINFOHANDLE;
@mem = map { substr $_, 0, 0 - length (" kB\n") } @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];
@@ -526,35 +572,35 @@ sub add_events () {
my $shmem = (split /\s+/xms, $mem[20])[1];
my $memused = int (($memtotal - $memavail) / ($memtotal / 100));
$meminfo = sprintf (
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;
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;
my $cputemp = readline $CPUTEMPHANDLE;
close $CPUTEMPHANDLE;
chomp $cputemp;
$cputemp = int $cputemp;
@@ -563,6 +609,15 @@ sub add_events () {
}
$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;
}
}
}
},
@@ -575,28 +630,25 @@ sub add_events () {
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}) {
$file_check = 1;
$vpnstr .= sprintf '<span foreground="%s">⍋</span>', $CONF->{vpn}->{up_color};
} else {
$file_check = 0;
$vpnstr .= sprintf '<span foreground="%s">⍒</span>', $CONF->{vpn}->{down_color};
}
$file_check_mtime = $mtime;
} else {
$mtime = 0;
$file_check = 0;
$vpnstr .= sprintf '<span foreground="%s">⍒</span>', $CONF->{vpn}->{down_color};
}
}
);
# 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
$vpnstr .= ':';
if (
my $sock = IO::Socket->new (
Domain => AF_INET,
@@ -609,31 +661,52 @@ sub add_events () {
) {
$sock->shutdown (SHUT_WR);
$sock->close ();
$tcp_check = 1;
$vpnstr .= sprintf '<span foreground="%s">✔</span>', $CONF->{vpn}->{up_color};
} else {
$tcp_check = 0;
$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'}) {
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 ($?) {
if ($CHILD_ERROR) {
# 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;
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;
}
}
@@ -648,6 +721,7 @@ sub add_events () {
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") {
@@ -666,6 +740,16 @@ sub add_events () {
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;
}
}
);
}
@@ -683,6 +767,20 @@ sub add_events () {
}
)
}
# 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', $PROCESS_ID;
}
}
);
}
return;
@@ -697,10 +795,10 @@ sub spawn (@) {
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';
# 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
@@ -714,8 +812,10 @@ sub spawn (@) {
if (@args > 0) {
exec $prg, @args;
exit 0;
} else {
exec $prg;
exit 0;
}
}
# else - we just unable to fork, so silently ignore event
@@ -772,7 +872,7 @@ sub readconf () {
my $FH;
unless (open $FH, '<', $file) {
die "Unable to open $file: $!\n";
die "Unable to open $file: $OS_ERROR\n";
}
my $len = (stat $file)[7];
@@ -783,7 +883,7 @@ sub readconf () {
close $FH;
unless (defined $readlen) {
die "Unable to read $file: $!\n";
die "Unable to read $file: $OS_ERROR\n";
}
if ($readlen != $len) {
@@ -793,7 +893,7 @@ sub readconf () {
my $config = eval { JSON::XS->new->utf8->relaxed->decode ($data) };
unless ($config) {
die "Unable to decode $file: $@\n";
die "Unable to decode $file: $EVAL_ERROR\n";
}
return $config;

View File

@@ -53,6 +53,9 @@
# state file and update its timestamp at least as frequent as mtime_threshold in seconds
"vpn": {
"enabled": 0,
# html (actually, pango) notation
"down_color" : "red",
"up_color" : "green",
"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
@@ -67,7 +70,10 @@
# 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,
"simple-volume-pa": {
"enabled": 0,
"symbol": "🎶"
},
"net-if" : {
"enabled" : 0,