From 96fc306ef79118db5f73dae8beb85d09de833c19 Mon Sep 17 00:00:00 2001 From: Sergei Fedosov Date: Fri, 12 Feb 2021 19:19:22 +0300 Subject: [PATCH] Add i3status-anyevent --- i3status-anyevent | 416 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100644 i3status-anyevent diff --git a/i3status-anyevent b/i3status-anyevent new file mode 100644 index 0000000..702c068 --- /dev/null +++ b/i3status-anyevent @@ -0,0 +1,416 @@ +#!/usr/bin/perl + +# 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 Linux::Inotify2; +use JSON::XS; +use Net::DBus; +use Encode; + +sub spawn (@); +sub find_caps_led (); +sub to_hours ($); +sub readconf (); + +my $CONF = readconf (); + +# note that /proc and sys does not generate events via inotify() +#my @files = qw( /proc/meminfo /proc/loadavg /tmp/boom /sys/class/net/wlan0/operstate ); +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 $output_string; +my $old_output_string = ''; +my $STDIN_FIRST_LINE_DROPPED = 0; + +#my $inotify = Linux::Inotify2->new or die "Can't create inotify object: $!"; +my $done = AnyEvent->condvar; +my $CAPS_LED_PATH = find_caps_led (); + +# prep dee-boos connection +my $bus = Net::DBus->system; +my $upower = $bus->get_service ('org.freedesktop.UPower'); +my $properties = $upower->get_object ('/org/freedesktop/UPower/devices/battery_BAT0', 'org.freedesktop.DBus.Properties'); + +# capture click events and do not override sigstop and sigcont numbers +syswrite STDOUT, << 'EOJSON'; +{"version": 1, "click_events": true} +[ [], +EOJSON + +#foreach my $file (@files) { +# $inotify->watch ( +# $file, +# IN_MODIFY, +# +# sub { +# my $e = shift; +## if ($e->IN_MODIFY) { +# print "$file modified\n"; +# } +# } +# ); +#} + +# Register callbacks/event handlers + +# Actually print messages there +my $watch_stdout = AnyEvent->timer ( + after => 1, # sleep 1 second + interval => 0.3, # then invoke every 1 second + cb => sub { # the callback to invoke + + my $OUTPUT; + @{$OUTPUT} = @{$CONF}; + + push @{$OUTPUT}, { "color" => "#3e78fd", "full_text" => $caps_state }; + push @{$OUTPUT}, { "color" => "#3e78fd", "full_text" => $cputemp }; + push @{$OUTPUT}, { "color" => "#3e78fd", "full_text" => $meminfo }; + push @{$OUTPUT}, { "color" => "#3e78fd", "full_text" => "LA: $la" }; + push @{$OUTPUT}, { "color" => "#3e78fd", "full_text" => $charge }; + push @{$OUTPUT}, { "color" => "#3e78fd", "full_text" => $wallclock }; + + $output_string = eval { encode_json ($OUTPUT) }; + + if (defined $output_string) { + if ($output_string ne $old_output_string) { + syswrite STDOUT, decode ('UTF-8', $output_string) . ",\n"; + $old_output_string = $output_string; + } + } else { + warn $@; + } + + @{$OUTPUT} = -1; + } +); + + +# read STDIN +my $watch_stdin = 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) { + foreach (@{$CONF}) { + if ((defined $_->{name}) && ($j->{name} eq $_->{name})) { + spawn ($_->{cmd}); + last; + } + } + } + } + } +); + +# poll caps lock status, on one of my laptop there is no led for this :( +my $watch_caps_led = 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 eq '2') { + $caps_state = '? caps'; + } else { + $caps_state = eval { readline ($CAPSHANDLE) }; + seek $CAPSHANDLE, SEEK_SET, 0; + chomp $caps_state; + + if ($caps_state eq '1') { + $caps_state = '☒ caps'; + } else { + $caps_state = '☐ caps'; + } + } + } +); + +# poll clock by timer +my $watch_clock = 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 dbus +# but it is possible to fetch this status via upower directly +my $watch_battery = AnyEvent->timer ( + after => 0, # first invoke ASAP + interval => 10, # then invoke every 10 seconds + cb => sub { # the callback to invoke + my $capacity = $properties->Get ('org.freedesktop.UPower.Device', 'Percentage'); + my $state = $properties->Get ('org.freedesktop.UPower.Device', 'State'); + my $time = '?'; + + if ($state == 2) { + # discharging + $time = $properties->Get ('org.freedesktop.UPower.Device', 'TimeToEmpty'); + # time in seconds, let's translate it to hours:minutes + $state = '▼'; + } elsif ($state == 1) { + # charging + $time = $properties->Get ('org.freedesktop.UPower.Device', 'TimeToFull'); + $state = '▲'; + } else { + # already charged + $time = 0; + $state = '•'; + } + + if ($time ne '?') { + $time = to_hours ($time); + } + + $charge = sprintf '⚡ %s%% %s %s', $capacity, $state, $time; + } +); + + +# put all fast accessable stats in one callback +my $watch_stats = AnyEvent->timer ( + after => 0, # first invoke ASAP + interval => 3, # then invoke every 3 second + cb => sub { # the callback to invoke + + # Fetch Load Average + 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 (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 $memavail = (split /\s+/xms, $mem[2])[1]; + my $swaptotal = (split /\s+/xms, $mem[14])[1]; + my $swapfree = (split /\s+/xms, $mem[15])[1]; + + $meminfo = sprintf ( + 'mem/sw: %s/%s M', + int ($memavail / 1024), + int (($swaptotal - $swapfree) / 1024) + ); + + # populate cpu temperature info + my $hwmon = 1; + + if (defined $CPUTEMPHANDLE) { + if (tell $CPUTEMPHANDLE == -1) { + if (open ($CPUTEMPHANDLE, '<', '/sys/class/hwmon/hwmon0/temp1_input')) { + $hwmon = 1; + } else { + $hwmon = 0; + } + } + } else { + open ($CPUTEMPHANDLE, '<', '/sys/class/hwmon/hwmon0/temp1_input') or do { + $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); + } + }, +); + + +# This callback for inotify stuff +#my $watch_file_event = AnyEvent->idle ( +# cb => sub { +# $inotify->poll; +# } +#); + +$done->recv(); + +sub spawn (@) { + my @args = @_; + my $pid = fork(); + + if ($pid) { + # Parent, work done, return + return; + } elsif ($pid == 0) { + # Child, we need to exec() given prog with args + + # 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 + my $prg = shift(@args); + # weird situation, silently quit here :) + exit 0 unless(defined($prg)); + + if (@args > 0) { + exec $prg @args; + } else { + exec $prg; + } + } else { # Unable to fork() + syswrite STDERR, "[{\"full_text\":\"ERROR: Could not fork new process: $!\",\"color\":\"#ff0000\"}],\n"; + } + } + + 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 $file = "$ENV{HOME}/.i3/config.json"; + open (my $FH, '<', $file) or 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 { decode_json ($data) }; + + if ($@) { + die "Unable to decode $file: $@\n"; + } + + return $config; +} \ No newline at end of file