#!/usr/bin/perl -w use strict; use Time::Local; # Number of horizontal calendars to draw. use constant CALENDAR_COUNT => 2; # Color bitmasks. use constant CURRENT_MONTH => 1; use constant CURRENT_DAY => 2; use constant FULL_MOON => 4; # All variables. my ($x, $y, $z, $mday, $month, $year, $wday, @current, @t, @colors, $i, $j, $k, $l, @c, @age); # Color palette, indexed by bitmask. my @palette = ( "\e[90;40m", "\e[37;40m", # CURRENT_MONTH "\e[30;47m", # CURRENT_DAY "\e[30;47m", # CURRENT_MONTH | CURRENT_DAY "\e[90;40m", # FULL_MOON (not current month) "\e[96;40m", # FULL_MOON | CURRENT_MONTH "\e[30;106m", # FULL_MOON | CURRENT_DAY # CURRENT_MONTH | CURRENT_DAY | FULL_MOON is handled specially. ); # Split timestamp into components. sub LocalTime() { return localtime $l; } # Align timestamp to midnight. sub Align() { ($x, $y, $z, $mday, $month, $year) = LocalTime; $l = timelocal 0, 0, 0, $k ? $mday : 1, $month, $year; } # Conditionally output a string. sub Output($$) { print $_[0] if $_[1]; } # Get dates for current month. @t = (time); if( !$#ARGV ) { if( $ARGV[0] !~ /^(\d+)-(\d+)-(\d+)$/ || !defined($t[0] = eval { timelocal(0, 0, 0, $3, $2 - 1, $1 - 1900) }) ) { die "Unable to parse $ARGV[0] as YYYY-MM-DD\n"; } } for($i = 0; $i < CALENDAR_COUNT; $i++) { $l = $t[$i]; ($x, $y, $z, $mday, $month, $year) = LocalTime; push @current, [$year, $month, $i ? -1 : $mday]; # Go back to first day of the month, then go back to first Sunday. $k = 0; for(Align;; Align) { ($x, $y, $z, $mday, $month, $k, $wday) = LocalTime; last if !$wday; $l -= 43200; } $t[$i] = $l; @c = @age = (); for($j = 0; $j < 6 * 7; $j++) { ($x, $y, $z, $mday, $month, $year, $wday, $k) = LocalTime; # Record moon age for each date. This is calculated based on # number of days since 1900-01-01, which happens to be a new moon. # An easier way to compute this is to just take the number of # unix seconds and do a modulus, but that relies on the unix # epoch being fixed, which isn't true for all platforms. $k += $year * 365 + int(~-$year / 4) - int(~-$year / 100) + int(($year + 299) / 400); # Length of synodic month: # https://en.wikipedia.org/wiki/Lunar_phase#Calculating_phase use constant PERIOD => 29.530588853; push @age, $k - int($k / PERIOD) * PERIOD; # Set color based on whether displayed date matches current date. push @c, ($month - $current[$i][1] ? 0 : $mday - $current[$i][2] ? CURRENT_MONTH : CURRENT_DAY); $l += 86400 + 43200; Align; } # Now update colors based on whether moon age between midnights contains # 15.5, and also add padding for bottommost row. for($j = 0; $j < 6 * 7 - 1; $j++) { $c[$j] |= FULL_MOON if $age[$j] <= 15.5 && $age[$j + 1] > 15.5; push @c, 0; } push @colors, [@c]; # Draw headings. Output " ", $i; printf "\e[97;40m\e[4m %04d-%02d" . (" " x 26) . "\e[0m", $current[$i][0] + 1900, $current[$i][1] + 1; # Step forward 6 weeks from first Sunday of current month. This is # guaranteed to put us somewhere in the next month, and definitely not # the month after that. push @t, $t[$i] + 86400 * 6 * 7; } Output "\n", 1; # Draw calendars. for($j = 0; $j < 7; $j++) { # Draw odd rows. for($i = 0; $i < CALENDAR_COUNT; $i++) { Output " ", $i; for($k = 0; $k < 7; $k++) { $x = $colors[$i][$j * 7 + $k]; $y = $j * 7 + $k > 6 ? $colors[$i][$j * 7 + $k - 7] : 0; $z = $x | $y; # Special handling for when row above or below is the current date, # and the other row is a full moon. We can't simply use a bitwise # OR for this because we need to distinguish the two cases: # (CURRENT_MONTH|FULL_MOON) | CURRENT_DAY -> CURRENT_DAY # (CURRENT_DAY|FULL_MOON) | CURRENT_MONTH -> CURRENT_MONTH|FULL_MOON $z = $z - (CURRENT_MONTH | CURRENT_DAY | FULL_MOON) ? $z : CURRENT_DAY | ($x - CURRENT_DAY && $y - CURRENT_DAY ? FULL_MOON : 0); Output "$palette[$z] \e[0m ", 1; } } Output "\n", 1; # Draw even rows. if( $j < 6 ) { for($i = 0; $i < CALENDAR_COUNT; $i++) { $l = $t[$i]; Output " ", $i; for($wday = 0; $wday < 7; $wday++) { ($x, $y, $z, $mday) = LocalTime; printf '%s %2d '."\e[0m ", $palette[$colors[$i][$j * 7 + $wday]], $mday; $l += 86400 + 43200; Align; } $t[$i] = $l; } Output "\n", 1; } }