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.

287 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 --file was invoked, get time from targetted file instead of taking
  52. # the first param as a date:
  53. my %dates;
  54. if ($date_from_file) {
  55. my $file = File::Spec->rel2abs($ARGV[0]);
  56. say "Date from file: $file";
  57. if ($file =~ m{^.*?/diary/(\d{4}-\d{2}-\d{2})[.]wiki$}) {
  58. # Handle vimwiki diary entries:
  59. $dates{$1} .= '(vimwiki)';
  60. } elsif ($file =~ m{^.*?/p1k3/archives/(\d{4})/(\d+)/(\d+)(/index)?$}) {
  61. # Handle p1k3 entries:
  62. $dates{ sprintf('%d-%02d-%02d', $1, $2, $3) } .= "(p1k3)";
  63. }
  64. # Just use the file's modification time:
  65. $dates{ get_mtime_day($ARGV[0]) } .= "(mtime)";
  66. my $git_date = git_commit_date($file);
  67. if ($git_date) {
  68. # Handle git commit dates:
  69. $dates{ $git_date } .= "(git commit)";
  70. }
  71. } else {
  72. $dates{ $ARGV[0] } .= "(arg)";
  73. }
  74. foreach my $date (keys %dates) {
  75. unless ($date =~ m/\d+-\d+-\d+/) {
  76. say "invalid date: $date";
  77. next;
  78. }
  79. my $day = Time::Piece->strptime($date, $ISO_8601_FMT);
  80. print_day_slice($day, $dates{$date});
  81. }
  82. sub print_day_slice {
  83. my ($day, $label) = @_;
  84. my $next_day = $day + ONE_DAY;
  85. say "${day} ${label} {{{";
  86. print_section('p1k3', p1k3($day));
  87. print_section('vimwiki', vimwiki($day));
  88. print_section('shell', commandlog($day, $next_day));
  89. print_section('pinboard', pinboard($day));
  90. print_section('myrepos', myrepos($day, $next_day));
  91. say "}}}";
  92. }
  93. sub print_section {
  94. my ($title, @content) = @_;
  95. return unless defined $content[0];
  96. say "${title} {{{";
  97. foreach my $thing (@content) {
  98. say $thing;
  99. }
  100. say "}}}";
  101. }
  102. sub commandlog {
  103. my ($day, $next_day) = @_;
  104. my $after = $day->strftime('%Y-%m-%d 00:00:00');
  105. my $before = $next_day->strftime('%Y-%m-%d 00:00:00');
  106. my (@cl_lines) = `commandlog log --after="${after}" --before="${before}"`;
  107. if (@cl_lines > 1) {
  108. return join "", @cl_lines;
  109. }
  110. return;
  111. }
  112. sub myrepos {
  113. my ($day, $next_day) = @_;
  114. my $after = $day->strftime('%Y-%m-%d 00:00:00');
  115. my $before = $next_day->strftime('%Y-%m-%d 00:00:00');
  116. my (@mr_lines) = `cd ~ && mr -m log --stat --since="${after}" --before="${before}" 2>&1`;
  117. if (@mr_lines > 1) {
  118. return join "", @mr_lines;
  119. }
  120. return;
  121. }
  122. sub p1k3 {
  123. my ($day) = @_;
  124. my $daypath = $day->strftime(
  125. "%Y/%-m/%-d"
  126. );
  127. if (-e File::HomeDir->my_home . '/p1k3/archives/' . $daypath) {
  128. return join "", `cd ~/p1k3 && wrt display ${daypath} | lynx -stdin -dump`
  129. }
  130. return;
  131. }
  132. sub pinboard {
  133. my ($day) = @_;
  134. # See the bit about auth here:
  135. # https://pinboard.in/api/
  136. my $home = File::HomeDir->my_home;
  137. my $token = file_get_contents("${home}/.pinboardrc");
  138. chomp($token);
  139. my $url = $day->strftime(
  140. 'https://api.pinboard.in/v1/posts/get?dt=%Y-%m-%d&meta=yes&format=json&auth_token='
  141. . $token
  142. );
  143. my $pinboard_json = `curl -s '${url}'`;
  144. my $JSON = JSON->new->utf8->pretty;
  145. my $pinboard_hashref = $JSON->decode($pinboard_json);
  146. my @posts = @{ $pinboard_hashref->{posts} };
  147. my @output;
  148. # my @output = (
  149. # "source: curl -s -K ${home}/.pinboardrc '${url}'"
  150. # );
  151. foreach my $post (@posts)
  152. {
  153. my $display = wrap("", "", $post->{description}) . "\n"
  154. if length($post->{description});
  155. $display .= wrap("\t", "\t", $post->{extended}) . "\n"
  156. if length($post->{extended});
  157. $display .= "\t" . $post->{href} . "\n";
  158. $display .= wrap("\t", "\t", $post->{tags}) . "\n"
  159. if length($post->{tags});
  160. push @output, $display;
  161. }
  162. if (@output > 0) {
  163. return join "\n", @output;
  164. }
  165. return;
  166. }
  167. sub vimwiki {
  168. my ($day) = @_;
  169. my $daypath = $day->strftime(
  170. File::HomeDir->my_home . "/notes/vimwiki/diary/%Y-%m-%d.wiki"
  171. );
  172. if (-e $daypath) {
  173. return ("source: $daypath", file_get_contents($daypath));
  174. }
  175. }
  176. # PHP-style file-content grabbing:
  177. sub file_get_contents {
  178. my ($file) = @_;
  179. open my $fh, '<', $file
  180. or croak "Couldn't open $file: $!\n";
  181. my $contents;
  182. {
  183. # line separator:
  184. local $/ = undef;
  185. $contents = <$fh>;
  186. }
  187. close $fh or croak "Couldn't close $file: $!";
  188. # TODO: _May_ want to assume here that any file is UTF-8 text.
  189. # http://perldoc.perl.org/perlunitut.html
  190. # return decode('UTF-8', $contents);
  191. return $contents;
  192. }
  193. # Horked from WRT::Date.
  194. sub get_mtime_day
  195. {
  196. use POSIX qw(strftime);
  197. my ($file) = @_;
  198. # my( $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
  199. # $atime, $mtime, $ctime, $blksize, $blocks )
  200. # = stat( $filename );
  201. my $mtime = (stat $file)[9];
  202. # return strftime("%Y-%m-%dT%H:%M:%SZ", localtime($mtime));
  203. return strftime("%Y-%m-%d", localtime($mtime));
  204. }
  205. # Get a commit date for a given path:
  206. sub git_commit_date {
  207. my ($path) = @_;
  208. my $dirname = dirname($path);
  209. say $path;
  210. if (in_git_worktree($path)) {
  211. my ($git_output) = `cd ${dirname} && git log -1 --format=%ci "${path}"`;
  212. my ($date) = $git_output =~ m/^(\d{4}-\d{2}-\d{2}).*$/;
  213. if ($date) {
  214. return $date;
  215. }
  216. }
  217. return;
  218. }
  219. # Is a given path in a git working tree?
  220. sub in_git_worktree {
  221. my ($path) = @_;
  222. my $dirname = dirname($path);
  223. return 0 unless -d "$dirname/.git";
  224. my ($git_check) = `cd "${dirname}" && git rev-parse --is-inside-work-tree`;
  225. if (defined $git_check) {
  226. chomp($git_check);
  227. return 1 if $git_check eq 'true';
  228. }
  229. return 0;
  230. }