#!/usr/bin/env perl
=pod
=head1 NAME
timeslice - aggregate CLI history, notes, blog entries, etc. for a given time
=head1 SYNOPSIS
# Find items for January 1st, 2019:
timeslice 2019-01-01
# Find items corresponding to a file's time - special cases some files
# with an embedded date, like a blog entry or a vimwiki diary page,
# otherwise falls back to mtime:
timeslice --file ~/notes/vimwiki/diary/2019-01-02.wiki
timeslice --file ~/screenshots/foo.png
=head1 DESCRIPTION
timeslice is a utility, for now extremely specific to my personal setup, for
aggregating info from various systems by date. It currently looks up entries
in vimwiki, L (my blog), Pinboard bookmarks, command
line history using
L, and commits in
git repos registered with myrepos.
This code is hot garbage.
=head1 AUTHOR
L
=cut
use warnings;
use strict;
use 5.10.0;
# This seems to be necessary to print UTF-8 without warnings:
use open ':std', ':encoding(UTF-8)';
use Carp;
use Data::Dumper;
use Encode qw(decode encode);
use File::Basename;
use File::Find;
use File::HomeDir;
use File::Spec;
use Getopt::Long;
use JSON;
use LWP::Simple;
use Pod::Usage;
use Text::Wrap;
use Time::Piece;
use Time::Seconds;
$Text::Wrap::columns = 72;
my $ISO_8601_FMT = '%Y-%m-%d';
# Handle options, including help generated from the POD above:
my $date_from_file = 0;
GetOptions(
'file' => \$date_from_file,
'help' => sub { pod2usage(0) },
) or pod2usage(2);
if (! @ARGV) {
pod2usage(0);
}
# If --file was invoked, get time from targetted file instead of taking
# the first param as a date:
my %dates;
if ($date_from_file) {
my $file = File::Spec->rel2abs($ARGV[0]);
say "Date from file: $file";
if ($file =~ m{^.*?/diary/(\d{4}-\d{2}-\d{2})[.]wiki$}) {
# Handle vimwiki diary entries:
$dates{$1} .= '(vimwiki)';
} elsif ($file =~ m{^.*?/p1k3/archives/(\d{4})/(\d+)/(\d+)(/index)?$}) {
# Handle p1k3 entries:
$dates{ sprintf('%d-%02d-%02d', $1, $2, $3) } .= "(p1k3)";
}
# Just use the file's modification time:
$dates{ get_mtime_day($ARGV[0]) } .= "(mtime)";
my $git_date = git_commit_date($file);
if ($git_date) {
# Handle git commit dates:
$dates{ $git_date } .= "(git commit)";
}
} else {
$dates{ $ARGV[0] } .= "(arg)";
}
foreach my $date (keys %dates) {
unless ($date =~ m/\d+-\d+-\d+/) {
say "invalid date: $date";
next;
}
my $day = Time::Piece->strptime($date, $ISO_8601_FMT);
print_day_slice($day, $dates{$date});
}
sub print_day_slice {
my ($day, $label) = @_;
my $next_day = $day + ONE_DAY;
say "${day} ${label} {{{";
print_section('p1k3', p1k3($day));
print_section('vimwiki', vimwiki($day));
print_section('shell', commandlog($day, $next_day));
print_section('pinboard', pinboard($day));
print_section('myrepos', myrepos($day, $next_day));
say "}}}";
}
sub print_section {
my ($title, @content) = @_;
return unless defined $content[0];
say "${title} {{{";
foreach my $thing (@content) {
say $thing;
}
say "}}}";
}
sub commandlog {
my ($day, $next_day) = @_;
my $after = $day->strftime('%Y-%m-%d 00:00:00');
my $before = $next_day->strftime('%Y-%m-%d 00:00:00');
my (@cl_lines) = `commandlog log --after="${after}" --before="${before}"`;
if (@cl_lines > 1) {
return join "", @cl_lines;
}
return;
}
sub myrepos {
my ($day, $next_day) = @_;
my $after = $day->strftime('%Y-%m-%d 00:00:00');
my $before = $next_day->strftime('%Y-%m-%d 00:00:00');
my (@mr_lines) = `cd ~ && mr -m log --stat --since="${after}" --before="${before}" 2>&1`;
if (@mr_lines > 1) {
return join "", @mr_lines;
}
return;
}
sub p1k3 {
my ($day) = @_;
my $daypath = $day->strftime(
"%Y/%-m/%-d"
);
if (-e File::HomeDir->my_home . '/p1k3/archives/' . $daypath) {
return join "", `cd ~/p1k3 && wrt display ${daypath} | lynx -stdin -dump`
}
return;
}
sub pinboard {
my ($day) = @_;
# See the bit about auth here:
# https://pinboard.in/api/
my $home = File::HomeDir->my_home;
my $token = file_get_contents("${home}/.pinboardrc");
chomp($token);
my $url = $day->strftime(
'https://api.pinboard.in/v1/posts/get?dt=%Y-%m-%d&meta=yes&format=json&auth_token='
. $token
);
my $pinboard_json = `curl -s '${url}'`;
my $JSON = JSON->new->utf8->pretty;
my $pinboard_hashref = $JSON->decode($pinboard_json);
my @posts = @{ $pinboard_hashref->{posts} };
my @output;
# my @output = (
# "source: curl -s -K ${home}/.pinboardrc '${url}'"
# );
foreach my $post (@posts)
{
my $display = wrap("", "", $post->{description}) . "\n"
if length($post->{description});
$display .= wrap("\t", "\t", $post->{extended}) . "\n"
if length($post->{extended});
$display .= "\t" . $post->{href} . "\n";
$display .= wrap("\t", "\t", $post->{tags}) . "\n"
if length($post->{tags});
push @output, $display;
}
if (@output > 0) {
return join "\n", @output;
}
return;
}
sub vimwiki {
my ($day) = @_;
my $daypath = $day->strftime(
File::HomeDir->my_home . "/notes/vimwiki/diary/%Y-%m-%d.wiki"
);
if (-e $daypath) {
return ("source: $daypath", file_get_contents($daypath));
}
}
# PHP-style file-content grabbing:
sub file_get_contents {
my ($file) = @_;
open my $fh, '<', $file
or croak "Couldn't open $file: $!\n";
my $contents;
{
# line separator:
local $/ = undef;
$contents = <$fh>;
}
close $fh or croak "Couldn't close $file: $!";
# TODO: _May_ want to assume here that any file is UTF-8 text.
# http://perldoc.perl.org/perlunitut.html
# return decode('UTF-8', $contents);
return $contents;
}
# Horked from WRT::Date.
sub get_mtime_day
{
use POSIX qw(strftime);
my ($file) = @_;
# my( $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
# $atime, $mtime, $ctime, $blksize, $blocks )
# = stat( $filename );
my $mtime = (stat $file)[9];
# return strftime("%Y-%m-%dT%H:%M:%SZ", localtime($mtime));
return strftime("%Y-%m-%d", localtime($mtime));
}
# Get a commit date for a given path:
sub git_commit_date {
my ($path) = @_;
my $dirname = dirname($path);
say $path;
if (in_git_worktree($path)) {
my ($git_output) = `cd ${dirname} && git log -1 --format=%ci "${path}"`;
my ($date) = $git_output =~ m/^(\d{4}-\d{2}-\d{2}).*$/;
if ($date) {
return $date;
}
}
return;
}
# Is a given path in a git working tree?
sub in_git_worktree {
my ($path) = @_;
my $dirname = dirname($path);
return 0 unless -d "$dirname/.git";
my ($git_check) = `cd "${dirname}" && git rev-parse --is-inside-work-tree`;
if (defined $git_check) {
chomp($git_check);
return 1 if $git_check eq 'true';
}
return 0;
}