#!/usr/bin/env perl # # Dependencies (Linux): iproute2 iw volctl # Dependencies (OpenBSD): (none) # ################################################################################ use strict; use warnings; use feature 'signatures'; use Data::Dumper; use Time::HiRes qw; use List::Util 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})"; } ################################################################################ # Profiling sub show_time_delta($section, $time0, $computed) { my (undef, $time) = gettimeofday(); $time = $time0 if $time < $time0; my $diff = ($time - $time0) / 1000; my $extra = $computed ? ' (COMPUTED)' : ''; printf STDERR "Spent %.2f milliseconds @ $section$extra\n", $diff; return $diff; } ################################################################################ # Modules my %MODS; ########################################## # Linux my %linux_mods; $linux_mods{'CPU'} = { period => 5, default_opts => {'warn5' => 1, 'err5' => 3}, mkcompute => sub($opts) { open(my $loadf, '<', '/proc/loadavg'); return sub { 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], $opts->{warn5}, $opts->{err5}), }; }; }, }; $linux_mods{'MEM'} = { period => 5, default_opts => {'warn' => 70, 'err' => 90}, mkcompute => sub($opts) { return sub { my $mused = `free | awk '/^Mem:/ {printf("%d", 100 * (\$3/\$2))}'`; return { text => sprintf("MEM %s%%", $mused), color => color_thresholds($mused, $opts->{warn}, $opts->{err}), }; }; }, }; 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, default_opts => {'warn' => 30, 'err' => 10}, mkcompute => sub($opts) { my $BATDIR = glob '/sys/class/power_supply/BAT?'; open(my $bcapf, '<', "$BATDIR/capacity"); open(my $bstatf, '<', "$BATDIR/status"); return sub { 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, 100-$opts->{warn}, 100-$opts->{err}), }; }; }, }; # TODO: remove volctl dep $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, default_opts => {'warn5' => 1, 'err5' => 3}, mkcompute => sub($opts) { return 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], $opts->{warn5}, $opts->{err5}), }; }; }, }; $openbsd_mods{'MEM'} = { period => 5, default_opts => {'warn' => 70, 'err' => 90}, mkcompute => sub($opts) { return 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, $opts->{warn}, $opts->{err}), }; }; }, }; sub render_iface_openbsd($ifaces, $ifname) { my $iface = $ifaces->{$ifname}; my $extra = $iface->{'ip'} ? " $iface->{'ip'}" : ''; my $is_wireless = exists $iface->{'wifi'}; if (exists $iface->{'wifi_ssid'}) { $extra .= ' (' . $iface->{'wifi_ssid'} . ', ' . $iface->{'wifi_signal'} . ')'; } return txt_onoff($is_wireless ? '๐Ÿ“ถ' : '๐ŸŒ', $iface->{'up'}) . $extra; } $openbsd_mods{'NET'} = { period => 4, mkcompute => sub { my @wired = (); my @wireless = (); my $ifname; for (`ifconfig`) { if (/^([a-z]+[0-9]+):/) { $ifname = $1; } elsif (/media: (Ethernet|IEEE802.11)/) { if ($1 eq 'Ethernet') { push @wired, $ifname; } else { push @wireless, $ifname; } } } return sub { my %ifaces; my $ifname; for (`ifconfig`) { if (/^([a-z]+[0-9]+):/) { if (grep { $_ eq $1 } @wired, @wireless) { $ifname = $1; $ifaces{$ifname}{'up'} = /UP/; } else { $ifname = undef; } } next unless defined($ifname); if (/ieee80211:/) { $ifaces{$ifname}{'wifi'} = 1; if (/join "([^"]*)".* ([0-9]+%)/) { $ifaces{$ifname}{'wifi_ssid'} = $1; $ifaces{$ifname}{'wifi_signal'} = $2; } } elsif (/inet ([0-9.]+)/) { $ifaces{$ifname}{'ip'} = $1; } } return { text => join ' ยท ', map { render_iface_openbsd(\%ifaces, $_) } @wired, @wireless }; }; }, }; sub obsd_get_sysctl($mib) { my @parts = split(' ', `sysctl -n $mib`); return $parts[0]; } $openbsd_mods{'BAT'} = { period => 3, default_opts => {'warn' => 30, 'err' => 10}, mkcompute => sub($opts) { return 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, 100-$opts->{warn}, 100-$opts->{err}), }; }; }, }; $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 @user_mods; my $profile = 0; foreach my $arg (@ARGV) { if ($arg eq '--profile') { $profile = 1; } else { push @user_mods, $arg; } } sub render_section($result) { my $color = $result->{color}; $color = $color ? ",\"$color->{type}\":\"$color->{val}\"" : ''; return sprintf "{\"full_text\":\"%s\",\"separator\":false$color}", $result->{text}; } my $separator = render_section({text => 'ยท'}); # Remove opts from @user_mods # Call each module's mkcompute subroutine if it exists foreach my $modspec (@user_mods) { my ($modname, @modopts) = split(',', $modspec); $modspec = $modname; my $mod = $os_mods{$modname} or die "ERROR: '$modname' is not a valid module.\n"; my $default_opts = $mod->{default_opts} || {}; my %opts = (%$default_opts, map { split '=', $_ } @modopts); $mod->{compute} = $mod->{mkcompute}(\%opts) if exists $mod->{mkcompute}; } printf("{ \"version\": 1 }\n[\n"); my @timedeltas; my %cache; my $counter = 0; while (1) { my @sections; my (undef, $time0) = gettimeofday() if $profile; foreach my $modname (@user_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; } if ($profile) { push @timedeltas, show_time_delta('TOTAL', $time0, 0); printf STDERR "AVG/ROUND: %.2f ms\n", (sum @timedeltas) / @timedeltas; } printf("[%s],\n", join(",$separator,", @sections)); $counter += 1; sleep 1; } # vim: set sw=4