7 hlcal - Highlight BSD cal(1) output
9 hlncal - Highlight BSD ncal(1) output
13 hlcal [OPTIONS] [CAL-OPTIONS]
15 hlncal [OPTIONS] [NCAL-OPTIONS]
19 Wrap cal(1), ncal(1) around and highlight specific days.
27 =item I<DATE>=I<COLOR>
29 =item I<START-DATE>...I<END-DATE>[,I<DOW>[,I<DOW>[,...]]]=I<COLOR>
33 Where I<DOW> is a day-of-week name (3 letters),
34 I<COLOR> is a space- or hyphen-delimited list of ANSI color or other
35 formatting style name,
36 I<DATE> (and I<START-DATE>, I<END-DATE>) is in I<[[YYYY-]MM-]DD> format,
37 ie. year and month are optional,
38 and lack of them interpreted as "every year" and "every month"
41 In single date definition, I<DATE>, may enter an asterisk C<*> as month
42 to select a given date in every month in the given year, or in every
43 year if you leave out the year as well.
46 In the interval definition, may add several I<DOW> days which makes only
47 those days highlighted in the specified interval.
49 C<04-01...06-30,WED> means every Wednesday in the second quarter.
50 C<1...7,FRI> means the first Friday in every month.
52 =head1 SUPPORTED ANSI COLORS AND STYLES
54 Colors: black, red, green, yellow, blue, magenta, cyan, white, default.
56 May be preceded by C<bright>, eg: C<bright red>.
57 May be followed by C<bg> to set the background color instead of the
58 foreground, eg: C<yellow-bg>.
60 Styles: bold, faint, italic, underline,
61 blink_slow, blink_rapid, inverse, conceal, crossed,
63 Note, not all styles are supported by all terminal emulators.
67 hlncal today=inverse `ncal -e`=yellow_bg-red SUN=bright-red SAT=red -bM3
73 use Getopt
::Long qw
/:config no_ignore_case bundling no_getopt_compat pass_through/;
74 use feature qw
/switch/;
75 no if ($] >= 5.018), 'warnings' => 'experimental::smartmatch';
78 use List
::MoreUtils qw
/all any none/;
80 use POSIX qw
/strftime setlocale LC_TIME/;
82 use DateTime
::Duration
;
83 use DateTime
::Format
::Strptime
;
88 # take the basename of the command as procname.
89 $0 =~ s/.*\/([^\/]+)$/$1/;
120 my $re_months = join '|', keys %Months;
121 my $re_mon = qq/(0?[1-9]|1[012]|$re_months)/;
122 my $re_dom = q
/(0[1-9]|[12]\d|3[01])/;
123 my $re_dow = 'MON|TUE|WED|THU|FRI|SAT|SUN';
127 black
=>30, red
=>31, green
=>32, yellow
=>33, blue
=>34, magenta
=>35, cyan
=>36, white
=>37,
130 'reset'=>0, bold
=>1, faint
=>2, italic
=>3, underline
=>4,
131 blink_slow
=>5, blink_rapid
=>6, inverse
=>7, conceal
=>8, crossed
=>9,
133 noitalic
=>23, nounderline
=>24, noblink
=>25, noinverse
=>27, noconceal
=>28, nocrossed
=>29,
141 $str =~ s/[- ]bg/_bg/g;
142 $str =~ s/bright[- ]/bright_/g;
143 for my $word (split /[\s-]+/, $str)
146 if($word =~ /^(?'BRIGHT'bright_|)(?'COLOR'.+?)(?'BG'_bg|)$/ and any
{$_ eq $+{'COLOR'}} (keys %ANSIcolor))
148 $code += 10 if $+{'BG'};
149 $code += 60 if $+{'BRIGHT'};
152 if(exists $ANSIcode{$word}) { $code += $ANSIcode{$word}; }
160 my @codes = grep {length} get_ansi_codes
join " ", @_;
161 return '' if not @codes;
162 return sprintf "\x1B[%sm", join(';', @codes);
163 #return join '', map { sprintf "\x1B[%sm", $_ } @codes;
169 warn "$0: no ANSI code: $color\n" unless all
{length} get_ansi_codes
$color;
173 $OptNoHighlightToday = 0;
174 # weeks starts on Sunday by default in cal
175 $week_start_on = SUN
;
176 $week_start_day_explicite = 0;
178 $calcmd = 'ncal' if $0 =~ /ncal/;
183 my $arg = shift @ARGV;
185 # TODO: get highlight parameters from config files
186 # TODO: mix dow/date/interval definitions under @Highlight list
188 $arg =~ s
{(today
|yesterday
|tomorrow
)(?
=.*=)}{
190 $date = DateTime
->now(time_zone
=>'local')->strftime('%F') if $date eq 'today';
191 $date = DateTime
->now(time_zone
=>'local')->subtract(days
=>1)->strftime('%F') if $date eq 'yesterday';
192 $date = DateTime
->now(time_zone
=>'local')->add(days
=>1)->strftime('%F') if $date eq 'tomorrow';
196 if(my ($day, $color) = $arg =~ /^($re_dow)=(.+)$/i)
198 $Highlight->{'days-of-week'}->{eval $day} = $color;
201 elsif($arg =~ /^(((?'YEAR'\d\d\d\d)-|)(?'MON'$re_mon|\*)-|)(?'DOM'$re_dom)=(?'COLOR'.+)$/i)
203 my $year = $+{'YEAR'} || '*';
204 my $mon = $+{'MON'} || '*';
206 my $color = $+{'COLOR'};
209 $mon = $Months{$mon} if exists $Months{$mon};
210 $Highlight->{'date'}->{$year}->{$mon}->{$dom} = $color;
213 elsif($arg =~ /^(((?'Y1'\d\d\d\d)-|)((?'M1'$re_mon)-)|)(?'D1'$re_dom)\.\.\.(?('M1')(?('Y1')((?'Y2'\d\d\d\d)-|))((?'M2'$re_mon)-|))(?'D2'$re_dom)(?'DOW'(,($re_dow))*)=(?'COLOR'.+)$/i)
215 push @
{$Highlight->{'interval'}}, {
216 'start' => {'y'=>$+{'Y1'}, 'm'=>$Months{lc$+{'M1'}}||$+{'M1'}, 'd'=>$+{'D1'}},
217 'end' => {'y'=>$+{'Y2'}, 'm'=>$Months{lc$+{'M2'}}||$+{'M2'}, 'd'=>$+{'D2'}},
218 'dow' => [map {eval} grep {$_} split /,/, $+{'DOW'}],
219 'color' => $+{'COLOR'},
221 verify_color
$+{'COLOR'};
225 pod2usage
(-exitval
=>2, -verbose
=>99);
230 # give it back to cal/ncal.
236 #warn Dumper $Highlight; # DEBUG
242 'M' => sub { $week_start_on = MON
; $week_start_day_explicite = 1; },
243 'S' => sub { $week_start_on = SUN
; $week_start_day_explicite = 1; },
244 'C' => sub { $calcmd = 'cal'; },
245 'N' => sub { $calcmd = 'ncal'; },
246 'b' => sub { $layout = HORIZONTAL
; },
247 'h' => \
$OptNoHighlightToday, # TODO
249 pod2usage
(-exitval
=>'NOEXIT', -verbose
=>99);
250 system("sh", "-c", "cal --help 2>&1");
253 ) or pod2usage
(-exitval
=>2, -verbose
=>99);
256 if(not defined $layout)
258 $layout = HORIZONTAL
;
259 $layout = VERTICAL
if $calcmd eq 'ncal';
262 if(!$week_start_day_explicite and $calcmd eq 'ncal')
264 # ncal takes locale settings for the week's first day
265 ($first_weekday) = `locale -k first_weekday` =~ /=(\d+)/;
266 if(defined $first_weekday)
268 $week_start_on = ($first_weekday+5) % 7;
274 my $month_parser = new DateTime
::Format
::Strptime
(pattern
=>'%Y %b', on_error
=>'croak', locale
=>setlocale
(LC_TIME
));
278 # if this bsdcal version supports -h flag,
279 if(system("$calcmd -h >/dev/null 2>&1") == 0)
281 # prevent cal to highlight today. we will do it.
282 unshift @cal_args, "-h";
285 open my $pipe, '-|:utf8', $calcmd, @cal_args or croak
"$0: popen: $calcmd: $!";
286 binmode STDOUT
, ':utf8';
299 my @words = /([^\d\s]+)/g;
300 if(scalar @words >= 1 and scalar @words <= 3 and (not /\d/ or /\d\d\d\d/))
302 # seems to be 1, 2, or 3 month names
304 for my $word (@words)
306 push @month_column, int $month_parser->parse_datetime("$year $word")->strftime('%m');
310 my @years = /(\d\d\d\d)/g;
313 @year_column = @years;
316 if(/([^\d\s]+\s+){7}/)
318 # at least 7 groups of letters (not space or digit),
319 # it's probably day of week names;
320 # assuming horizontal layout.
322 # TODO: any locale setting in which month names have space?
324 $layout = HORIZONTAL
;
332 for my $dow ($week_start_on, MON
..SUN
)
334 my $repeats = ($dow - $week_start_on) % 7 + 1;
335 /^(\s*[^\d\s]+){$repeats}/;
336 $dow_end{$dow} = length $&;
337 $dow_bgn{$dow} = $dow eq $week_start_on ?
0 : $dow_end{($dow-1)%7} + $day_spacing;
346 if($layout eq HORIZONTAL
)
348 if(/\d/ and $title_passed)
350 # at least 1 number -> a row of a week
357 my $month_spacing = 2;
358 my $where_last_dow_column_ends = $dow_end{($week_start_on-1)%7};
359 my $how_many_month_columns = int(length($_) / $where_last_dow_column_ends);
362 for my $month_column (0..$how_many_month_columns-1)
364 my $week_start_offset = $month_column * ($where_last_dow_column_ends + $month_spacing);
366 my %hldow = map {$_=>$Highlight->{'days-of-week'}->{$_}} keys %{$Highlight->{'days-of-week'}};
367 my $cur_year = $year_column[$month_column] || $year;
368 my $cur_month = $month_column[$month_column];
370 for my $dow ( $week_start_on .. SUN
, MON
.. ($week_start_on-1) )
372 my $dom = int substr($_, $dow_bgn{$dow} + $week_start_offset + $accum_shift, $dow_end{$dow}-$dow_bgn{$dow});
374 my $this_date = DateTime
->new(year
=>$cur_year, month
=>$cur_month, day
=>$dom, time_zone
=>'local');
376 for my $ival (@
{$Highlight->{'interval'}})
378 next if @
{$ival->{'dow'}} and not $dow ~~ @
{$ival->{'dow'}};
380 my $s = $ival->{'start'};
381 my $e = $ival->{'end'};
383 my $sy = $s->{'y'}||$cur_year;
384 my $sm = $s->{'m'}||$cur_month;
385 my $start = DateTime
->new(year
=>$sy, month
=>$sm, day
=>$s->{'d'}, time_zone
=>'local');
386 my $end = DateTime
->new(year
=>$e->{'y'}||$sy, month
=>$e->{'m'}||$sm, day
=>$e->{'d'}, time_zone
=>'local');
390 if (! $e->{'m'}) { $end->add(months
=>1); }
391 elsif(! $e->{'y'}) { $end->add(years
=>1); }
393 if($this_date < $start)
395 if (! $s->{'m'}) { $_->subtract(months
=>1) for $start, $end; }
396 elsif(! $s->{'y'}) { $_->subtract(years
=>1) for $start, $end; }
399 #warn $this_date->ymd, ": ", $start->ymd, "...", $end->ymd, "\n"; # DEBUG
401 if($this_date >= $start and $this_date <= $end)
403 $hldow{$dow} .= " " . $ival->{'color'};
409 $dates{$d} = $c while ($d, $c) = each %{$Highlight->{'date'}->{'*'}->{'*'}};
410 $dates{$d} = $c while ($d, $c) = each %{$Highlight->{'date'}->{'*'}->{$cur_month}};
411 $dates{$d} = $c while ($d, $c) = each %{$Highlight->{'date'}->{$cur_year}->{$cur_month}};
412 for my $date (keys %dates)
414 my $dow = int strftime
('%u', 0, 0, 0, $date, $cur_month-1, $cur_year-1900) - 1;
415 my $date_at_dow_location = substr($_, $dow_bgn{$dow} + $week_start_offset + $accum_shift, $dow_end{$dow}-$dow_bgn{$dow});
416 $hldow{$dow} .= " " . $dates{$date} if $date == $date_at_dow_location;
419 for my $dow ( $week_start_on .. SUN
, MON
.. ($week_start_on-1) )
421 if(exists $hldow{$dow})
423 my $color = $hldow{$dow};
424 my $insert1 = ansicode
($color);
425 my $insert2 = ansicode
('reset');
426 my $bgn = $dow_bgn{$dow} + $week_start_offset + $accum_shift;
427 my $end = $dow_end{$dow} + $week_start_offset + $accum_shift;
429 $_ = substr($_, 0, $bgn) . $insert1 . substr($_, $bgn, $end-$bgn) . $insert2 . substr($_, $end);
430 $accum_shift += length($insert1) + length($insert2);
437 else # $layout eq VERTICAL
441 # no 1 or 2 digit numbers, assuming month name(s) header.
442 # TODO: any locale setting in which month names have numbers?
444 $day_of_week = $week_start_on - 1;
450 # at least 4 numbers -> it's a new row of days
451 $day_of_week = ($day_of_week + 1) % 7;
457 # TODO: hilight dates
459 while (my ($hldow, $color) = each %{$Highlight->{'days-of-week'}})
461 if($day_of_week == $hldow)
463 s/^/ansicode($color)/e;
464 s/$/ansicode('reset')/e;