#!/usr/bin/env perl # # Dependencies: iproute2 iw volctl # ################################################################################ use strict; use warnings; use feature 'signatures'; use Time::HiRes qw; # Turn off buffering $| = 1; ################################################################################ # Formatting use constant { COLOR_WARN => "#ffff44", COLOR_ERR => "#ff6600", }; sub color_thresholds($x, $warn, $err) { if ($x >= $err) { return { type => 'background', val => COLOR_ERR }; } elsif ($x >= $warn) { return { type => 'color', val => COLOR_WARN }; } else { return 0; } } sub txt_onoff($txt, $on) { return $on ? $txt : "${txt}X"; } ################################################################################ # Profiling sub show_time_delta($section, $time0, $computed) { my (undef, $time) = gettimeofday(); my $diff = ($time - $time0) / 1000; my $extra = $computed ? ' (COMPUTED)' : ''; printf STDERR "Spent $diff milliseconds @ $section$extra\n"; } ################################################################################ # Modules my %MODS; ########################################## # Linux my %linux_mods; $linux_mods{'CPU'} = { period => 5, init => sub { open(my $loadf, '<', '/proc/loadavg'); return { state => $loadf }; }, compute => sub { my $loadf = shift; read($loadf, my $loads, 14); seek($loadf, 0, 0); my @loads = split ' ', $loads; return { text => sprintf ("CPU %s/%s/%s", @loads), color => color_thresholds($loads[1], 1, 3), state => $loadf, }; } }; $linux_mods{'MEM'} = { period => 5, compute => sub { my $mused = `free | awk '/^Mem:/ {printf("%d", 100 * (\$3/\$2))}'`; return { text => sprintf("MEM %s%%", $mused), color => color_thresholds($mused, 70, 90), }; } }; sub render_iface_linux($is_wireless, $name, $status, $addr) { my $extra = $addr ? " $addr" : ''; if ($is_wireless and $status eq 'UP') { my ($ssid) = `iw dev $name link` =~ /SSID: ([^\n]*).*/; my ($dbm) = `iw dev $name link` =~ /signal: ([0-9-]+ dBm)/; $extra .= ' (' . $ssid . ', ' . $dbm . ')'; } return txt_onoff($is_wireless ? '๐Ÿ›œ' : '๐ŸŒ', $status eq 'UP') . $extra; } $linux_mods{'NET'} = { period => 4, compute => sub { my @wired = (); my @wireless = (); for (split("\n", `ip --br a`)) { next if substr($_, 0, 2) eq 'lo'; # prevent regex my ($if, $status, $addr) = $_ =~ /^(\S+)\s+(\S+)(?:\s+(\S+))?/; push @wired, [0, $if, $status, $addr] if (substr($if, 0, 2) eq 'en'); push @wireless, [1, $if, $status, $addr] if (substr($if, 0, 2) eq 'wl'); } return { text => join ' | ', map { render_iface_linux(@$_) } @wired, @wireless }; } }; $linux_mods{'BAT'} = { period => 3, init => sub { my $BATDIR = glob '/sys/class/power_supply/BAT?'; open(my $bcapf, '<', "$BATDIR/capacity"); open(my $bstatf, '<', "$BATDIR/status"); return { state => [$bcapf, $bstatf] }; }, compute => sub { my $state = shift; my ($bcapf, $bstatf) = @$state; my $batlevel = <$bcapf>; chomp $batlevel; seek($bcapf, 0, 0); my $batcharging = <$bstatf> eq "Charging\n" ? 'โšก' : ''; seek($bstatf, 0, 0); return { text => sprintf("๐Ÿ”‹%s%s", $batlevel, $batcharging), color => color_thresholds(100 - $batlevel, 70, 90), state => $state, }; } }; $linux_mods{'AUDIO'} = { period => 2, compute => sub { return { text => sprintf "%s%s", txt_onoff('๐Ÿ”Š', do {`~/.local/bin/volctl vol-on`; $? == 0}), txt_onoff('๐ŸŽค', do {`~/.local/bin/volctl mic-on`; $? == 0}) }; } }; $MODS{'Linux'} = \%linux_mods; ########################################## # OpenBSD my %openbsd_mods; $openbsd_mods{'CPU'} = { period => 5, compute => sub { my $loads = `sysctl -n vm.loadavg`; chomp $loads; my @loads = split ' ', $loads; return { text => sprintf ("CPU %s/%s/%s", @loads), color => color_thresholds($loads[1], 1, 3), }; } }; $openbsd_mods{'MEM'} = { period => 5, compute => sub { my $uvmexp = `vmstat -s`; my ($mtotal) = $uvmexp =~ /([0-9]+) pages managed/; my ($mused) = $uvmexp =~ /([0-9]+) pages active/; $mused = 100 * $mused / $mtotal; return { text => sprintf("MEM %d%%", $mused), color => color_thresholds($mused, 70, 90), }; } }; sub render_iface_openbsd($ifaces, $ifname) { my $iface = $ifaces->{$ifname}; my $extra = $iface->{'ip'} ? " $iface->{'ip'}" : ''; my $is_wireless = exists $iface->{'wifi'}; my $is_up = $iface->{'up'}; if ($is_wireless and $is_up) { $extra .= ' (' . $iface->{'wifi_ssid'} . ', ' . $iface->{'wifi_signal'} . ')'; } return txt_onoff($is_wireless ? '๐Ÿ“ถ' : '๐ŸŒ', $is_up) . $extra; } $openbsd_mods{'NET'} = { period => 4, compute => sub { my @wired = (); my @wireless = (); my %ifaces; my $ifname; for (`ifconfig`) { if (/^([a-z]+[0-9]+):/) { $ifname = $1; $ifaces{$ifname} = {}; $ifaces{$ifname}{'up'} = /UP/; } elsif (/media: (.*)/) { $ifaces{$ifname}{'media'} = $1; } elsif (/ieee80211:.* join "([^"]*)".* ([0-9]+%)/) { $ifaces{$ifname}{'wifi'} = 1; $ifaces{$ifname}{'wifi_ssid'} = $1; $ifaces{$ifname}{'wifi_signal'} = $2; } elsif (/inet ([0-9.]+)/) { $ifaces{$ifname}{'ip'} = $1; } } foreach my $ifname (keys %ifaces) { next if $ifname =~ /^lo[0-9]+/; my $iface = $ifaces{$ifname}; next unless $iface->{'ip'} || $iface->{'media'}; if ($iface->{'wifi'}) { push @wireless, $ifname; } else { push @wired, $ifname; } } return { text => join ' ยท ', map { render_iface_openbsd(\%ifaces, $_) } sort(@wired), @wireless }; } }; sub obsd_get_sysctl($mib) { my @parts = split(' ', `sysctl -n $mib`); return $parts[0]; } $openbsd_mods{'BAT'} = { period => 3, compute => sub { my $batcharging = obsd_get_sysctl('hw.sensors.acpibat0.raw0') eq "0" ? 'โšก' : ''; my $batmax = obsd_get_sysctl('hw.sensors.acpibat0.watthour0'); my $batcur = obsd_get_sysctl('hw.sensors.acpibat0.watthour3'); my $batlevel = 100 * $batcur / $batmax; return { text => sprintf("๐Ÿ”‹%d%%%s", $batlevel, $batcharging), color => color_thresholds(100 - $batlevel, 70, 90) }; } }; $openbsd_mods{'AUDIO'} = { period => 2, compute => sub { my $audioinfo = `sndioctl`; my ($muted) = $audioinfo =~ /output\.mute=([01])/; my ($micmuted) = $audioinfo =~ /input\.mute=([01])/; return { text => sprintf "%s %s", txt_onoff('๐Ÿ”Š', $muted == 0), txt_onoff('๐ŸŽค', $micmuted == 0) }; } }; $MODS{'OpenBSD'} = \%openbsd_mods; ########################################## # Common (POSIX) my %common_mods; $common_mods{'DATE'} = { period => 2, compute => sub { my $date = `date +'%Y-%m-%d %H:%M'`; chomp $date; return { text => $date }; } }; $MODS{'_common'} = \%common_mods; ################################################################################ # Main my $OS = `uname -s`; chomp $OS; my %os_mods = ( %{$MODS{$OS}}, %{$MODS{'_common'}} ); my @mods = ('CPU', 'MEM', 'NET', 'BAT', 'AUDIO', 'DATE'); my %cache; sub render_section($result) { my $color = $result->{color}; my $color = $color ? ",\"$color->{type}\":\"$color->{val}\"" : ''; return sprintf "{\"full_text\":\"%s\",\"separator\":false$color}", $result->{text}; } my $separator = render_section({text => 'ยท'}); my $profile = 0; foreach my $arg (@ARGV) { if ($arg eq '--profile') { $profile = 1; } } printf("{ \"version\": 1 }\n[\n"); # Call each module's init subroutine foreach my $modname (@mods) { my $mod = $os_mods{$modname}; $cache{$modname} = &{$mod->{init}} if $mod->{init}; } my $counter = 0; while (1) { my @sections; my (undef, $time0) = gettimeofday() if $profile; foreach my $modname (@mods) { my $mod = $os_mods{$modname}; my (undef, $time) = gettimeofday() if $profile; my $result = $cache{$modname}; if ($counter % $mod->{period} == 0) { $result = &{$mod->{compute}}($result->{state}); $cache{$modname} = $result; } push(@sections, render_section($result)); show_time_delta($modname, $time, $counter % $mod->{period} == 0) if $profile; } show_time_delta('TOTAL', $time0, 0) if $profile; printf("[%s],\n", join(",$separator,", @sections)); $counter += 1; sleep 1; } # vim: set sw=4