Dotfiles, utilities, and other apparatus.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

291 lines
6.7 KiB

  1. #!/usr/bin/env perl
  2. =pod
  3. =head1 NAME
  4. timeslice - aggregate CLI history, notes, blog entries, etc. for a given time
  5. =head1 SYNOPSIS
  6. # Find items for January 1st, 2019:
  7. timeslice 2019-01-01
  8. # Find items corresponding to a file's time - special cases some files
  9. # with an embedded date, like a blog entry or a vimwiki diary page,
  10. # otherwise falls back to mtime:
  11. timeslice --file ~/notes/vimwiki/diary/2019-01-02.wiki
  12. timeslice --file ~/screenshots/foo.png
  13. =head1 DESCRIPTION
  14. timeslice is a utility, for now extremely specific to my personal setup, for
  15. aggregating info from various systems by date. It currently looks up entries
  16. in vimwiki, L<p1k3|https://p1k3.com/> (my blog), Pinboard bookmarks, command
  17. line history using
  18. L<commandlog|https://code.p1k3.com/gitea/brennen/commandlog>, and commits in
  19. git repos registered with myrepos.
  20. This code is hot garbage.
  21. =head1 AUTHOR
  22. L<Brennen Bearnes|https://p1k3.com/>
  23. =cut
  24. use warnings;
  25. use strict;
  26. use 5.10.0;
  27. # This seems to be necessary to print UTF-8 without warnings:
  28. use open ':std', ':encoding(UTF-8)';
  29. use Carp;
  30. use Data::Dumper;
  31. use Encode qw(decode encode);
  32. use File::Basename;
  33. use File::Find;
  34. use File::HomeDir;
  35. use File::Spec;
  36. use Getopt::Long;
  37. use JSON;
  38. use LWP::Simple;
  39. use Pod::Usage;
  40. use Text::Wrap;
  41. use Time::Piece;
  42. use Time::Seconds;
  43. $Text::Wrap::columns = 72;
  44. my $ISO_8601_FMT = '%Y-%m-%d';
  45. # Handle options, including help generated from the POD above:
  46. my $date_from_file = 0;
  47. GetOptions(
  48. 'file' => \$date_from_file,
  49. 'help' => sub { pod2usage(0) },
  50. ) or pod2usage(2);
  51. if (! @ARGV) {
  52. pod2usage(0);
  53. }
  54. # If --file was invoked, get time from targetted file instead of taking
  55. # the first param as a date:
  56. my %dates;
  57. if ($date_from_file) {
  58. my $file = File::Spec->rel2abs($ARGV[0]);
  59. say "Date from file: $file";
  60. if ($file =~ m{^.*?/diary/(\d{4}-\d{2}-\d{2})[.]wiki$}) {
  61. # Handle vimwiki diary entries:
  62. $dates{$1} .= '(vimwiki)';
  63. } elsif ($file =~ m{^.*?/p1k3/archives/(\d{4})/(\d+)/(\d+)(/index)?$}) {
  64. # Handle p1k3 entries:
  65. $dates{ sprintf('%d-%02d-%02d', $1, $2, $3) } .= "(p1k3)";
  66. }
  67. # Just use the file's modification time:
  68. $dates{ get_mtime_day($ARGV[0]) } .= "(mtime)";
  69. my $git_date = git_commit_date($file);
  70. if ($git_date) {
  71. # Handle git commit dates:
  72. $dates{ $git_date } .= "(git commit)";
  73. }
  74. } else {
  75. $dates{ $ARGV[0] } .= "(arg)";
  76. }
  77. foreach my $date (keys %dates) {
  78. unless ($date =~ m/\d+-\d+-\d+/) {
  79. say "invalid date: $date";
  80. next;
  81. }
  82. my $day = Time::Piece->strptime($date, $ISO_8601_FMT);
  83. print_day_slice($day, $dates{$date});
  84. }
  85. sub print_day_slice {
  86. my ($day, $label) = @_;
  87. my $next_day = $day + ONE_DAY;
  88. say "${day} ${label} {{{";
  89. print_section('p1k3', p1k3($day));
  90. print_section('vimwiki', vimwiki($day));
  91. print_section('shell', commandlog($day, $next_day));
  92. print_section('pinboard', pinboard($day));
  93. print_section('myrepos', myrepos($day, $next_day));
  94. say "}}}";
  95. }
  96. sub print_section {
  97. my ($title, @content) = @_;
  98. return unless defined $content[0];
  99. say "${title} {{{";
  100. foreach my $thing (@content) {
  101. say $thing;
  102. }
  103. say "}}}";
  104. }
  105. sub commandlog {
  106. my ($day, $next_day) = @_;
  107. my $after = $day->strftime('%Y-%m-%d 00:00:00');
  108. my $before = $next_day->strftime('%Y-%m-%d 00:00:00');
  109. my (@cl_lines) = `commandlog log --after="${after}" --before="${before}"`;
  110. if (@cl_lines > 1) {
  111. return join "", @cl_lines;
  112. }
  113. return;
  114. }
  115. sub myrepos {
  116. my ($day, $next_day) = @_;
  117. my $after = $day->strftime('%Y-%m-%d 00:00:00');
  118. my $before = $next_day->strftime('%Y-%m-%d 00:00:00');
  119. my (@mr_lines) = `cd ~ && mr -m log --stat --since="${after}" --before="${before}" 2>&1`;
  120. if (@mr_lines > 1) {
  121. return join "", @mr_lines;
  122. }
  123. return;
  124. }
  125. sub p1k3 {
  126. my ($day) = @_;
  127. my $daypath = $day->strftime(
  128. "%Y/%-m/%-d"
  129. );
  130. if (-e File::HomeDir->my_home . '/p1k3/archives/' . $daypath) {
  131. return join "", `cd ~/p1k3 && wrt display ${daypath} | lynx -stdin -dump`
  132. }
  133. return;
  134. }
  135. sub pinboard {
  136. my ($day) = @_;
  137. # See the bit about auth here:
  138. # https://pinboard.in/api/
  139. my $home = File::HomeDir->my_home;
  140. my $token = file_get_contents("${home}/.pinboardrc");
  141. chomp($token);
  142. my $url = $day->strftime(
  143. 'https://api.pinboard.in/v1/posts/get?dt=%Y-%m-%d&meta=yes&format=json&auth_token='
  144. . $token
  145. );
  146. my $pinboard_json = `curl -s '${url}'`;
  147. my $JSON = JSON->new->utf8->pretty;
  148. my $pinboard_hashref = $JSON->decode($pinboard_json);
  149. my @posts = @{ $pinboard_hashref->{posts} };
  150. my @output;
  151. # my @output = (
  152. # "source: curl -s -K ${home}/.pinboardrc '${url}'"
  153. # );
  154. foreach my $post (@posts)
  155. {
  156. my $display = wrap("", "", $post->{description}) . "\n"
  157. if length($post->{description});
  158. $display .= wrap("\t", "\t", $post->{extended}) . "\n"
  159. if length($post->{extended});
  160. $display .= "\t" . $post->{href} . "\n";
  161. $display .= wrap("\t", "\t", $post->{tags}) . "\n"
  162. if length($post->{tags});
  163. push @output, $display;
  164. }
  165. if (@output > 0) {
  166. return join "\n", @output;
  167. }
  168. return;
  169. }
  170. sub vimwiki {
  171. my ($day) = @_;
  172. my $daypath = $day->strftime(
  173. File::HomeDir->my_home . "/notes/vimwiki/diary/%Y-%m-%d.wiki"
  174. );
  175. if (-e $daypath) {
  176. return ("source: $daypath", file_get_contents($daypath));
  177. }
  178. }
  179. # PHP-style file-content grabbing:
  180. sub file_get_contents {
  181. my ($file) = @_;
  182. open my $fh, '<', $file
  183. or croak "Couldn't open $file: $!\n";
  184. my $contents;
  185. {
  186. # line separator:
  187. local $/ = undef;
  188. $contents = <$fh>;
  189. }
  190. close $fh or croak "Couldn't close $file: $!";
  191. # TODO: _May_ want to assume here that any file is UTF-8 text.
  192. # http://perldoc.perl.org/perlunitut.html
  193. # return decode('UTF-8', $contents);
  194. return $contents;
  195. }
  196. # Horked from WRT::Date.
  197. sub get_mtime_day
  198. {
  199. use POSIX qw(strftime);
  200. my ($file) = @_;
  201. # my( $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
  202. # $atime, $mtime, $ctime, $blksize, $blocks )
  203. # = stat( $filename );
  204. my $mtime = (stat $file)[9];
  205. # return strftime("%Y-%m-%dT%H:%M:%SZ", localtime($mtime));
  206. return strftime("%Y-%m-%d", localtime($mtime));
  207. }
  208. # Get a commit date for a given path:
  209. sub git_commit_date {
  210. my ($path) = @_;
  211. my $dirname = dirname($path);
  212. say $path;
  213. if (in_git_worktree($path)) {
  214. my ($git_output) = `cd ${dirname} && git log -1 --format=%ci "${path}"`;
  215. my ($date) = $git_output =~ m/^(\d{4}-\d{2}-\d{2}).*$/;
  216. if ($date) {
  217. return $date;
  218. }
  219. }
  220. return;
  221. }
  222. # Is a given path in a git working tree?
  223. sub in_git_worktree {
  224. my ($path) = @_;
  225. my $dirname = dirname($path);
  226. return 0 unless -d "$dirname/.git";
  227. my ($git_check) = `cd "${dirname}" && git rev-parse --is-inside-work-tree`;
  228. if (defined $git_check) {
  229. chomp($git_check);
  230. return 1 if $git_check eq 'true';
  231. }
  232. return 0;
  233. }