#!/usr/bin/env perl
use 5.008;
use strict;
use utf8;
use warnings qw(all);

use List::Util qw(max sum);
use Storable qw(lock_retrieve lock_store);

## no critic (ProhibitBacktickOperators ProhibitComplexRegexes ProhibitInteractiveTest)
our $VERSION = 0.5;

my @ticks = qw[▁ ▂ ▃ ▄ ▅ ▆ ▇ █];
binmode \*STDOUT => q(encoding(utf8));

my $order   = '';
my $screen  = 0;
my $tmux    = not -t \*STDOUT;

my ($help, $show_battery, $remaining, $bolt, $chart_bg, $chart_fg, $bright, $loadavg, $threshold, $width);
if (@ARGV) {
    require Getopt::Long;
    Getopt::Long::GetOptions(
        q(help)         => \$help,
        q(battery!)     => \$show_battery,
        q(remaining!)   => \$remaining,
        q(bolt!)        => \$bolt,
        q(bg=s)         => \$chart_bg,
        q(fg=s)         => \$chart_fg,
        q(bright!)      => \$bright,
        q(loadavg!)     => \$loadavg,
        q(max=i)        => \$threshold,
        q(order=s)      => \$order,
        q(screen)       => \$screen,
        q(tmux!)        => \$tmux,
        q(width=i)      => \$width,
    ) or help(-verbose => 1);
}

defaults();

my $history = qq($ENV{HOME}/.rainbarf.dat);

my $chart = eval { lock_retrieve $history };
$chart = [ (0) x $width ] unless defined $chart;

my %top;
my $cpu = $loadavg ? load() : usage();

push @$chart => $cpu;
splice @$chart => 0, @$chart - $width if @$chart > $width;
lock_store $chart => $history;

unshift @$chart => (0) x ($width - @$chart);

# Ordering from Activity Monitor.app
my %colors = (
    f   => [ green  => 1 ], # free
    w   => [ red    => 2 ], # wired
    a   => [ yellow => 3 ], # active
    i   => [ blue   => 4 ], # inactive
    c   => [ cyan   => 5 ], # cached
);

# Custom ordering
my @order = split //x => lc $order;
for my $i (0 .. $#order) {
    my $color = $order[$i];
    $colors{$color}->[1] = $i + 1
        if exists $colors{$color};
}

my %memory = memory();

my $scale = @$chart / sum values %memory;
my $norm = $#ticks / max @$chart => $threshold;
my $c = 0;
for my $type (sort { $colors{$a}->[1] <=> $colors{$b}->[1] } keys %memory) {
    print my_color(
        map { defined $_ ? $_ : $colors{$type}->[0] }
        $chart_fg => $chart_bg
    );

    for (1 .. max(1 => sprintf q(%.0f) => $scale * $memory{$type})) {
        last if $c++ >= $width;
        print $ticks[$norm * shift @$chart];
    }
}
print $ticks[$norm * $cpu] if $c < $width;

battery() if $show_battery;

if ($screen) {
    print qq(\5{= dd});
} elsif ($tmux) {
} else {
    print Term::ANSIColor::color(q(reset)) => qq(\n);
}

# For dependencies linting
#printf qq(%-20s\t%s\n), $_ => $INC{$_} for sort keys %INC;

sub help {
    my (@args) = @_;
    require Pod::Usage;
    return Pod::Usage::pod2usage(-noperldoc => 1, @args);
}

sub defaults {
    help(-verbose => 99)    if $help;
    $show_battery   = 1     unless defined $show_battery;
    $loadavg        = 0     unless defined $loadavg;
    $threshold      = 1     unless defined $threshold;
    $width          = 38    unless defined $width;
    $bright         = 0     unless defined $bright;
    $bolt           = (defined $bolt and $bolt) ? q(⚡) : q(|);
    require Term::ANSIColor if not $tmux and not $screen;

    return;
}

sub my_color {
    my ($fg, $bg) = @_;
    my $out;

   if ($screen) {
        $out = qq(\5{);
        $out .= defined($bg)
            ? chr ord $bg
            : q(.);
        $out .= $bright
            ? uc chr ord $fg
            : chr ord $fg;
        $out .= q(});
    } elsif ($tmux) {
        $fg .= q(,bright) if $bright;
        $out = qq(#[fg=$fg]);
        $out .= qq(#[bg=$bg]) if defined $bg;
    } else {
        $fg = qq(bright_$fg) if $bright;
        $out = Term::ANSIColor::color(defined($bg) ? qq($fg on_$bg) : $fg);
    }

    return $out;
}

sub top {
    if (not %top and -x q{/usr/bin/top}) {
        for (qx{/usr/bin/top -R -F -l1 -n0 -s0}) {
            my ($key, $value) = /^([\w\s]+)\s*:\s*(.+)/x;
            next unless defined $key;
            $key =~ y/A-Z /a-z_/;
            $top{$key} = $value;
        }
    }
    return scalar keys %top;
}

sub load {
    my @l = qw(0 0 0);
    if (open my $loadavg, q{<}, q{/proc/loadavg}) {
        @l = (split /\s+/x, <$loadavg>)[0 .. 2];
        close $loadavg;
    } elsif (top() and exists $top{load_avg}) {
        @l = split /\s*,\s*/x, $top{load_avg};
    } elsif (-x q{/usr/bin/uptime}) {
        # fallback
        @l = (split /\s+/x, qx{/usr/bin/uptime})[-3 .. -1];
    }
    return $l[0];
}

sub usage {
    if (-e q{/proc/stat}) {
        my ($diff_usage, $prev_idle, $prev_total) = qw(-1 0 0);
        for my $i (reverse 0 .. 1) {
            my $fh;
            unless (open $fh, q{<}, q{/proc/stat}) {
                close $fh;
                return 0;
            }
            while (<$fh>) {
                next unless /^cpu\s+\d+/x;
                my @cpu = split /\s+/x;
                shift @cpu;

                my $idle = $cpu[3];
                my $total = sum(@cpu);

                my $diff_idle = $idle - $prev_idle;
                my $diff_total = $total - $prev_total;
                $diff_usage = ($diff_total - $diff_idle) / $diff_total;

                $prev_idle = $idle;
                $prev_total = $total;

                last;
            }
            close $fh;
            sleep $i;
        }
        return $diff_usage;
    } elsif (top()) {
        my %usage =
            map { (/([\d\.]+)%\s+(\w+)/x) [1, 0] }
            split /\s*,\s*/x, $top{cpu_usage};
        return 1 - $usage{idle} / 100;
    }
}

sub memory {
    my %m;
    if (exists $top{physmem}) {
        my %n = (K => 1/2**10, M => 1, G => 2**10);
        %m =
            map { $_->[2] => $_->[0] * $n{$_->[1]} }
            map { [(/(\d+)([KMG])\s+(\w)/x)] }
            split /\s*,\s*/x, $top{physmem};
        delete $m{u};
    } elsif (open my $meminfo, q{<}, q{/proc/meminfo}) {
        %m = map {
            /\b(?:Mem)?(Free|Cached|SwapCached|Active|Inactive):\s*(\d+)/ix
                ? (lc chr ord $1 => $2)
                : ();
        } <$meminfo>;
        $m{c} -= delete $m{s};
        close $meminfo;
    } elsif (-x q{/usr/bin/vm_stat}) {
        # fallback
        %m = map {
            /\bPages\s+(free|active|inactive|speculative|wired\s+down):\s*(\d+)/ix
                ? (chr ord $1 => $2 << 2)
                : ();
        } qx{/usr/bin/vm_stat};
        $m{f} += delete $m{s};
    }
    return %m;
}

sub battery {
    my ($battery, $charging, $time);
    if (-x q{/usr/sbin/ioreg}) {
        my %battery = map {
            /"(TimeRemaining|(?:Max|Current)Capacity|FullyCharged|ExternalConnected)"\s*=\s*(\d+|Yes|No)/ix
                ? (lc chr ord $1 => $2)
                : ()
        } qx{/usr/sbin/ioreg -n AppleSmartBattery -r};

        if ($battery{f} eq q(No)
            || ($battery{f} eq q(No) && $battery{e} eq q(Yes))
            || ($battery{f} eq q(Yes) && $battery{e} eq q(No))) {
            $time = $battery{t};
        }
        $charging = ($battery{e} =~ /^y/ix);
        $battery = eval { $battery{c} / $battery{m} };
    } elsif (-d q(/proc/acpi/battery)) {
        my ($acpi_info, $acpi_state, %battery) = ('') x 2;
        if (grep {
                -d $_
                and -e ($acpi_info  = qq($_/info))
                and -e ($acpi_state = qq($_/state))
            } sort glob q(/proc/acpi/battery/BAT[0-9])
        ) {
            for my $file ($acpi_info, $acpi_state) {
                my $fh;
                if (open $fh, q(<), $file) {
                    while (<$fh>) {
                        my ($key, $value) = /^([\w\s]+)\s*:\s*(\w+)/x;
                        next unless defined $key;
                        $key =~ y/A-Z /a-z_/;
                        $battery{$key} = $value;
                    }
                }
                close $fh;
            }

            $charging = $battery{charging_state} ne q(discharging);
            $time = eval { ($battery{remaining_capacity} / $battery{present_rate}) * 60 }
                if not $charging
                and defined $battery{present_rate}
                and $battery{present_rate} =~ /^\d+$/x;
            $battery = eval { $battery{remaining_capacity} / $battery{last_full_capacity} };
        }
    }

    battery_print($battery, $charging, $time);
    return;
}

sub battery_print {
    my ($battery, $charging, $time) = @_;
    if (defined $battery) {
        print my_color(($charging ? q(green) : q(red)), q(black)) => $bolt;
        if ($battery < 0.333) {
            print my_color(q(red));
        } elsif ($battery < 0.666) {
            print my_color(q(yellow));
        } else {
            print my_color(q(green));
        }
        print $ticks[$#ticks * $battery];
    }
    if (defined $remaining && defined $time) {
        my $sec = $time * 60;
        print " " . (($sec/(60*60))%24) . "h" . (($sec/60)%60) . "m ";
    }
    return;
}

__DATA__
=pod

=head1 NAME

rainbarf - CPU/RAM/battery stats chart bar for tmux (and GNU screen)

=head1 VERSION

version 0.5

=head1 SYNOPSIS

 rainbarf --tmux --width 40 --no-battery

=head1 DESCRIPTION

Fancy resource usage charts to put into the L<tmux|http://tmux.sourceforge.net/> status line.
The CPU utilization history chart is tinted with the following colors to reflect the system memory allocation:

=over 4

=item * B<green>: free memory;

=item * B<yellow>: active memory;

=item * B<blue>: inactive memory;

=item * B<red>: wired memory on I<Mac OS X>;

=item * B<cyan>: cached memory on I<Linux>.

=back

If available, battery charge is displayed on the right.

Just go to L<https://github.com/creaktive/rainbarf> to see some screenshots.

=head1 USAGE

=head2 Installation

 perl Build.PL
 ./Build test
 ./Build install

=head2 Configuration

Add the following line to your F<~/.tmux.conf> file:

 set -g status-right '#(rainbarf)'

Or, under I<GNOME Terminal>:

 set -g status-right '#(rainbarf --bright)'

Reload the tmux config by running C<tmux source-file ~/.tmux.conf>.

=head1 OPTIONS

=over 4

=item --help

This.

=item --[no]battery

Display the battery charge indicator.

=item --[no]remaining

Display the time remaining until the battery is fully charged/empty. See L</CAVEAT>.

=item --[no]bolt

Display even fancier battery indicator.

=item --[no]bright

Tricky one. Disabled by default. See L</CAVEAT>.

=item --fg COLOR_NAME

Force chart foreground color.

=item --bg COLOR_NAME

Force chart background color.

=item --[no]loadavg

Use L<load average|https://en.wikipedia.org/wiki/Load_(computing)> metric instead of CPU utilization.
You might want to set the C<--max> threshold since this is an absolute value and has varying ranges on different systems.

=item --max NUMBER

Maximum C<loadavg> you expect before rescaling the chart. Default is 1.

=item --order INDEXES

Specify the memory usage bar order.
The default is C<fwaic> ( B<f>ree, B<w>ired, B<a>ctive, B<i>nactive & B<c>ached ).

=item --[no]tmux

Force C<tmux> colors mode.
By default, L<rainbarf> detects automatically if it is being called from C<tmux> or from the interactive shell.

=item --screen

L<screen(1)|http://manpages.ubuntu.com/manpages/hardy/man1/screen.1.html> colors mode. B<Experimental>. See L</CAVEAT>.

=item --width NUMBER

Chart width. Default is 38, so both the chart and the battery indicator fit the C<tmux> status line.
Higher values may require disabling the battery indicator or raising the C<status-right-length> value in F<~/.tmux.conf>.

=back

=head1 CAVEAT

=head2 Time remaining

If the C<--remaining> option is present but you do not see the time in your status bar, you may need to increase the value of C<status-right-length> to 48.

=head2 Color scheme

If you only see the memory usage bars but no CPU utilization chart, that's because your terminal's color scheme need an explicit distinction between foreground and background colors.
For instance, "red on red background" will be displayed as a red block on such terminals.
Thus, you may need the ANSI B<bright> attribute for greater contrast.
There are two problems with it, though:

=over 4

=item 1.

Other color schemes (notably, L<solarized|http://ethanschoonover.com/solarized>) have different meaning for the ANSI B<bright> attribute.
So using it will result in a quite psychedelic appearance.

=item 2.

The older versions of L<Term::ANSIColor> dependency do not recognize it at all, resulting in a confusing error message I<Invalid attribute name bright_yellow at ...>.
However, the whole L<Term::ANSIColor> is optional, it is only required to preview the effects of the L</OPTIONS> via command line before actually editing the F<~/.tmux.conf>.
That is, C<rainbarf --bright --tmux> B<is guaranteed to work> despite the outdated L<Term::ANSIColor>!

=back

=head2 Persistent storage

CPU utilization stats are persistently stored in the F<~/.rainbarf.dat> file.
Every L<rainbarf> execution will update and rotate that file.
Since C<tmux> calls L<rainbarf> periodically (every 15 seconds, by default), the chart will display CPU utilization for the last ~9.5 minutes (15 * 38).
Thus, several C<tmux> instances running simultaneously for the same user will result in a faster chart scrolling.

=head2 screen

Stable C<screen> version unfortunately has a broken UTF-8 handling specifically for the status bar.
Thus, I have only tested the L<rainbarf> with the variant from L<git://git.savannah.gnu.org/screen.git>.
My F<~/.screenrc> contents:

 backtick 1 15 15 rainbarf --bright --screen
 hardstatus string "%1`"
 hardstatus lastline

=head1 REFERENCES

=over 4

=item *

L<top(1)|http://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/top.1.html> is used to get the CPU/RAM stats if no F</proc> filesystem is available.

=item *

L<ioreg(8)|http://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/ioreg.8.html> is used to get the battery status on I<Mac OS X>.

=item *

L<ACPI|http://www.tldp.org/howto/acpi-howto/usingacpi.html> is used to get the battery status on I<Linux>.

=item *

L<Battery|https://github.com/Goles/Battery> was a source of inspiration.

=item *

L<Spark|http://zachholman.com/spark/> was another source of inspiration.

=back

=head1 AUTHOR

Stanislaw Pusep <stas@sysd.org>

=head1 CONTRIBUTORS

=over 4

=item *

L<Clemens Hammacher|https://github.com/hammacher>

=item *

L<Joe Hassick|https://github.com/jh3>

=item *

L<Tuomas Jormola|https://github.com/tjormola>

=back

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2013 by Stanislaw Pusep <stas@sysd.org>.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
