meter-reading/ui.pl

217 lines
5.8 KiB
Perl
Executable File

#!/usr/bin/perl
# Based on
# /usr/share/doc/libterm-readline-gnu-perl/examples/fileman.gz
use DBI;
use English qw(-no_match_vars);
use List::Util qw( reduce );
use Term::ExtendedColor qw(:all);
use Term::ReadLine;
use Time::Piece;
use Try::Tiny;
use common::sense;
# Seems to be necessary for utf8 to print correctly when running under tmux.
# Thanks to 'fuzzix' on #perl.
#
binmode STDOUT, ':encoding(UTF-8)';
#-------------------------------------------------------------------------------
# Globals.
my $dbh = DBI->connect(
"dbi:Pg:dbname=meter_reading", '', '',
{ AutoCommit => 1, RaiseError => 1, PrintError => 0 },
);
my $command_by_name = {
g => {
func => \&com_give,
doc => "Give: record when you gave a meter reading to the supplier.",
long => <<EOF
Usage: g {E|G|W} TAKEN_DATE [GIVEN_DATE]
You can omit GIVEN_DATE if it's 'today', or use CTRL-t to type today's date.
EOF
},
h => {
func => \&com_help,
doc => "Help. Type 'h x' for help on command x.",
},
q => {
func => \&com_quit,
doc => "Quit.",
},
r => {
func => \&com_report,
doc => "Report recent readings.",
long => <<EOF
Usage: r [E|G|W] [ROWS]
Report ROWS recent readings, for the given fluid or for all.
EOF
},
t => {
func => \&com_take,
doc => "Take: record a reading you took from a meter.",
long => <<EOF
Usage: t {E|G|W} READING [TAKEN_DATE]
You can omit TAKEN_DATE if it's 'today', or use CTRL-t to type today's date.
EOF
},
};
my $term = Term::ReadLine->new('Meter Reading');
# Use this ref as a sentinel value.
my $quit = [];
#-------------------------------------------------------------------------------
# Functions for the line reader.
sub stripwhite {
my $string = shift;
$string =~ s/^\s*//;
$string =~ s/\s*$//;
return $string;
}
sub print_err {
my ($str) = @_;
warn sprintf("%s\n", fg("red3", $str));
return;
}
sub execute_line {
my ($line) = @_;
my ($verb, @nouns) = split(' ', $line);
my $cmd = $command_by_name->{$verb};
if (! defined $cmd) {
print_err("'$verb': no such command.");
return;
} else {
return $cmd->{func}->(@nouns);
}
}
#-------------------------------------------------------------------------------
# Command functions and helpers.
# The $id parameter is for use by com_take() and com_give(). They use
# it to report on the row that's just been taken or given.
sub com_report {
my ($fluid, $rowcount, $id) = @_;
# Normalize to (fluid, rowcount).
if ((defined $rowcount) && $rowcount =~ m/(e|g|w)/i) {
($fluid, $rowcount) = ($rowcount, $fluid);
} elsif ((defined $fluid) && $fluid =~ m/[0-9]/) {
$rowcount = $fluid;
$fluid = undef;
}
$fluid = ($fluid ? (uc $fluid) : undef);
$rowcount = ($rowcount ? $rowcount : 5);
my $sql = <<EOF;
SELECT id, fluid, when_taken, reading, dc_size, dc_unit, when_given
FROM mr_consumption
WHERE id = COALESCE(?, id) AND fluid = COALESCE(?, fluid)
ORDER BY when_taken DESC, when_entered DESC
LIMIT ?
EOF
my $rows = $dbh->selectall_arrayref($sql, {}, $id, $fluid, $rowcount);
printf(
"%5s %s %10s %10s %9s %10s\n",
map { underline(fg('darkred1', bg('grey93', $_))); } (
" Id", "F", "Taken ", " Reading", " Per day", "Given ",
),
);
for (my $i = 0; $i < (scalar @$rows); $i++) {
my ($id, $f, $t, $r, $s, $u, $g) = @{$rows->[$i]};
my $rowstr = sprintf(
"%5d %s %s %10.3f %5.3f %-3s %10s",
$id, $f, $t, $r, $s, $u, $g // "",
);
printf("%s\n", $i % 2 ? $rowstr : bg("mistyrose1", $rowstr));
}
return;
}
sub com_give {
my ($fluid, $when_taken, $when_given) = @_;
$when_given = localtime->ymd if (! $when_given);
my $sql = <<EOF;
SELECT give_reading(i_fluid => ?, i_when_taken => ?, i_when_given => ?)
EOF
my ($id) = $dbh->selectrow_array(
$sql, {}, (uc $fluid), $when_taken, $when_given
);
printf("Reading %d updated as given on %s.\n", $id, $when_given);
com_report($fluid, 1, $id);
return;
}
sub com_take {
my ($fluid, $reading, $when_taken) = @_;
$when_taken = localtime->ymd if (! defined $when_taken);
my $sql = <<EOF;
SELECT take_reading(i_fluid => ?, i_reading => ?, i_when_taken => ?)
EOF
my ($id) = $dbh->selectrow_array($sql, {}, (uc $fluid), $reading, $when_taken);
printf("Reading saved in row %d.\n", $id);
com_report($fluid, 1, $id);
return;
}
sub com_quit {
my $sure = $term->readline(clear() . italic("Confirm quit? [Y/n]") . " ");
if ($sure =~ m/^y?$/i) {
printf("%s\n", italic("Bye."));
return $quit;
}
return;
}
sub com_help {
my ($cmd) = @_;
my $help_text = do {
if (my $cmdref = $command_by_name->{$cmd}) {
"$cmd: $cmdref->{doc}" . "\n" . $cmdref->{long};
} else {
reduce {
$a . "$b: " . $command_by_name->{$b}->{doc} . "\n";
} "", (sort keys %$command_by_name);
}
};
printf("%s", fg('blue15', $help_text));
return;
}
#-------------------------------------------------------------------------------
# Main.
$term->MinLine(0); # disable implict call of add_history()
$term->add_defun(
'foo', # dunno what this name is for.
sub { $term->insert_text(localtime->ymd); },
ord "\ct",
);
printf("%s\n", italic("For help, type 'h' and press return."));
RUNCMD: while (1) {
my $line = $term->readline(
fg('yellow1', bg('green28', "mr>")) . " ",
);
my $s = stripwhite($line);
next RUNCMD if (! length($s));
$term->AddHistory($s) if ($term->can('AddHistory'));
my $result = try {
execute_line($s);
} catch {
# Postgres errors take several lines so print only the first.
my @warning_lines = split(/\n/, $_);
print_err($warning_lines[0]);
};
last RUNCMD if ($quit == $result);
}
exit(0);