diff options
| author | historia <[not public]> | 2026-06-11 23:48:02 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-11 23:48:02 -0400 |
| commit | ff0307c5423cd2f757c5be160f30fda1b1de32a3 (patch) | |
| tree | d145effe0cc28c8cc4d16c6027f094beb98c6f81 /when.pl | |
| parent | 974cced9bd78d60d1454717e7b2e664c7790f04e (diff) | |
| download | soon-ff0307c5423cd2f757c5be160f30fda1b1de32a3.tar.gz | |
stop tracking when.pl
Diffstat (limited to 'when.pl')
| -rw-r--r-- | when.pl | 2013 |
1 files changed, 0 insertions, 2013 deletions
diff --git a/when.pl b/when.pl deleted file mode 100644 index f5197c1..0000000 --- a/when.pl +++ /dev/null @@ -1,2013 +0,0 @@ -#!/usr/bin/perl - -binmode STDOUT, ":utf8"; # eliminates "Wide character in print" error in Czech -use open ":encoding(utf8)"; # otherwise utf8 in input files is read as if 1 character==1 byte - -use utf8; # Indicates that source can contain utf8, which we use for the Greek translation. -use locale; - -use strict; -use Getopt::Long; # Comes with the Perl distribution. - -#---------------------------------------------------------------- -# Defaults for the preferences: -#---------------------------------------------------------------- - -our %preferences=( - 'language'=>'en', # user's language; this is normally overridden by $LANG environment var. - 'past'=>-1, # how many days into the past the report extends - 'future'=>14, # ...and how far into the future - 'calendar'=>'~/.when/calendar', # where to find the calendar file - 'wrap'=>80, # 0 means don't wrap; otherwise, wrap display to this many columns - 'wrap_auto'=>0, # Try to detect width of terminal automatically, if it's a TTY. - 'wrap_max'=>-1, # If positive, sets a maximum with, overriding wrap_auto if necessary. - 'rows'=>40, # try to limit output to less than this number of lines - 'rows_auto'=>1, # Try to detect width of terminal automatically, if it's a TTY. - 'paging'=>1, # Use a pager, if it's a TTY and the output is too long. - 'header'=>1, # Print headers. - 'paging_less_options'=>'-rXFE', # Extra options for the pager, if the pager is "less." - 'editor'=>'emacs -nw', # editor - 'now'=>'', # pretend it's some other day today - 'filter_accents_on_output'=>!($ENV{TERM}=~m/(mlterm|xterm)/), - # ...since most Unix terminals show accented Unicode chars as garbage. - # Testing with rxvt 2.7.10 shows that this does seem necessary for rxvt; - # see https://github.com/bcrowell/when/pull/10 . - 'styled_output'=>1, # Do they want ANSI styling if the output is a TTY? - 'styled_output_if_not_tty'=>0, # Do they want ANSI styling if the output isn't a TTY? - 'calendar_today_style'=>'bold', # ANSI styling for today's date on the calendar. - 'items_today_style'=>'bold', # ANSI styling for today's items on the calendar. - 'prefilter'=>'', # pipe calendar file through this program before feeding it to When - 'monday_first'=>0, # display Monday rather than Sunday as the first day of the week? - 'orthodox_easter'=>0, # use Orthodox Eastern Church's date for easter? - 'neighboring_months'=>1, # print 3 months when doing a "when c"? - 'ampm'=>1, # use 12-hour time? - 'auto_pm'=>0, # if nonzero, then times with hours less than this are assumed to be PM - 'literal_only'=>0, # only display items given as literal dates? - 'test_expression'=>0, # used by 'make test' -); - -our %options=( - 'help'=>0, # print documentation - 'version'=>0, # print version number, and other info - 'bare_version'=>0, # print version number - 'make_filter_regex'=>0, # to make unicode filtering efficient - 'test_accent_filtering'=>0, # to make sure it really catches all the accented characters that are present in the translations -); - -# The following is for use by Getopt::Long. ! means negatable. =s means it requires a string value, =i an integer. -our %command_line_options = ( - 'help'=>\$options{'help'}, - 'version'=>\$options{'version'}, - 'bare_version'=>\$options{'bare_version'}, - 'make_filter_regex'=>\$options{'make_filter_regex'}, - 'test_accent_filtering'=>\$options{'test_accent_filtering'}, - 'language=s'=>\$preferences{'language'}, - 'past=i'=>\$preferences{'past'}, - 'future=i'=>\$preferences{'future'}, - 'calendar=s'=>\$preferences{'calendar'}, - 'wrap=i'=>\$preferences{'wrap'}, - 'wrap_auto!'=>\$preferences{'wrap_auto'}, - 'wrap_max=i'=>\$preferences{'wrap_max'}, - 'rows=i'=>\$preferences{'rows'}, - 'rows_auto!'=>\$preferences{'rows_auto'}, - 'paging!'=>\$preferences{'paging'}, - 'header!'=>\$preferences{'header'}, - 'paging_less_options=s'=>\$preferences{'paging_less_options'}, - 'editor=s'=>\$preferences{'editor'}, - 'now=s'=>\$preferences{'now'}, - 'calendar_today_style=s'=>\$preferences{'calendar_today_style'}, - 'items_today_style=s'=>\$preferences{'items_today_style'}, - 'prefilter=s'=>\$preferences{'prefilter'}, - 'filter_accents_on_output!'=>\$preferences{'filter_accents_on_output'}, - 'styled_output!'=>\$preferences{'styled_output'}, - 'styled_output_if_not_tty!'=>\$preferences{'styled_output_if_not_tty'}, - 'monday_first!'=>\$preferences{'monday_first'}, - 'orthodox_easter!'=>\$preferences{'orthodox_easter'}, - 'neighboring_months!'=>\$preferences{'neighboring_months'}, - 'ampm!'=>\$preferences{'ampm'}, - 'auto_pm=i'=>\$preferences{'auto_pm'}, - 'literal_only!'=>\$preferences{'literal_only'}, - 'test_expression=s'=>\$preferences{'test_expression'}, - 'test_mode!'=>\$preferences{'test_mode'}, -); - -#---------------------------------------------------------------- -# Strings are all collected here for ease of internationalization: -#---------------------------------------------------------------- - -# When adding characters to the following list, make sure to add them to -# UnicodeTools::filter(), then run "./when --make_filter_regex" and cut and paste the output into UnicodeTools::filter_out_accents(). -our $e_acute = "\x{e9}"; -our $u_circumflex = "\x{fb}"; - -# German characters: -our $A_uml = "\x{c4}"; -our $a_uml = "\x{e4}"; -our $O_uml = "\x{d6}"; -our $o_uml = "\x{f6}"; -our $U_uml = "\x{dc}"; -our $u_uml = "\x{fc}"; -our $s_zlig = "\x{df}"; - -# Polish characters: -our $a_polish = "\x{105}"; -our $c_polish = "\x{107}"; -our $e_polish = "\x{119}"; -our $l_polish = "\x{142}"; -our $n_polish = "\x{144}"; -our $o_polish = "\x{0f3}"; -our $s_polish = "\x{15b}"; -our $z_polish = "\x{17a}"; -our $zz_polish = "\x{17c}"; - -# Czech characters: -our $a_acute = "\x{e1}"; -our $i_acute = "\x{ed}"; -our $u_acute = "\x{fa}"; -our $y_acute = "\x{fd}"; -our $U_acute = "\x{da}"; -our $C_wedge = "\x{10c}"; -our $c_wedge = "\x{10d}"; -our $e_wedge = "\x{11b}"; -our $R_wedge = "\x{158}"; -our $r_wedge = "\x{159}"; - -# Danish characters: -our $a_ring = "\x{e5}"; -our $A_ring = "\x{c5}"; -our $o_slash = "\x{f8}"; -our $O_slash = "\x{d8}"; -our $ae = "\x{e6}"; -our $AE = "\x{c6}"; - -# Swedish characters: -our $a_ring = "\x{e5}"; -our $A_ring = "\x{c5}"; -our $a_uml = "\x{e4}"; -our $A_uml = "\x{c4}"; -our $o_uml = "\x{f6}"; -our $O_uml = "\x{d6}"; - -# Spanish characters: -our $A_acute = "\x{c1}"; -our $E_acute = "\x{c9}"; -our $I_acute = "\x{cd}"; -our $O_acute = "\x{d3}"; -our $U_acute = "\x{da}"; -our $a_acute = "\x{e1}"; -our $e_acute = "\x{e9}"; -our $i_acute = "\x{ed}"; -our $o_acute = "\x{f3}"; -our $u_acute = "\x{fa}"; -our $n_tilde = "\x{f1}"; - -# French characters: -our $a_grave = "\x{e0}"; - -# Romanian diacritics -our $A_breve = "\x{102}"; -our $A_circumflex = "\x{c2}" ; -our $I_circumflex = "\x{ce}" ; -our $S_commabelow = "\x{218}" ; -our $T_commabelow = "\x{21a}" ; -our $a_breve = "\x{103}"; -our $a_circumflex = "\x{e2}" ; -our $i_circumflex = "\x{ee}" ; -our $s_commabelow = "\x{219}" ; -our $t_commabelow = "\x{21b}" ; -# Some time S and T with comma below are not present in the font. -# Some people are using the cedilla gliphs instead. They are wrong -# (the gliphs). -our $quot_open = "\x{201e}" ; -our $quot_close = "\x{201c}" ; -our $quotalt_open = "\x{ab}" ; -our $quotalt_close = "\x{bb}" ; -our $nonbreaking_hyphen = "\x{2011}" ; - - -#********************************************************************** -# When adding a language, make sure to update the list of languages -# in the man page as well! -#********************************************************************** - -our %month_name = - ( - 'en'=>'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec', # English - ); -our %month_name_long = - ( - # English: - 'en'=>'January February March April May June July August September October November December', - # Polish: - ); -our %wday_name = - ( - 'en'=>'Sun Mon Tue Wed Thu Fri Sat', # English - ); - -#------------------------------------------------------------------------ -# This subroutine is used every time we need to print out some text in the -# user's chosen language. -#------------------------------------------------------------------------ -sub w { - my $what = shift; # can be either a string representing a key, or [key,language] - my @stuff = @_; - my $lingo = $preferences{'language'}; - my %strings; - if (ref $what) { - $lingo = $what->[1]; - $what = $what->[0]; - } - %strings = - ( - 'date_syntax_error'=>"The date %s is not in the required format, which is 'y m d', with blanks separating the three parts.", - 'multiple_wildcards_in_date'=>"The date %s contains more than one wildcard character *. Try testing variables instead of using wildcards.", - 'error_opening_prefs'=>"The preferences file %s exists, but there was an error opening it for input.", - 'syntax_err_in_prefs'=>"Syntax error in preferences file %s:\n%s", - 'prefs_file_not_found'=>"Preferences file %s not found.\n", - 'illegal_command'=>"Illegal command, %s\n", - 'error_opening_calendar'=>"Couldn't open the file %s for input\n", - 'error_prefiltering'=>"Couldn't execute the prefilter %s\n", - 'not_utf8'=>"The file %s does not appear to be encoded in utf8 (or ascii, which is a subset of utf8). The unix 'file' utility can probably tell you what its encoding is.\n", - 'yesterday'=>'yesterday', - 'today'=>'today', - 'tomorrow'=>'tomorrow', - 'syntax_error_in_calendar'=>"Syntax error in calendar: %s\n", - 'illegal_year'=>"Illegal year: %s\n", - 'illegal_month'=>"Illegal month: %s\n", - 'illegal_day_of_month'=>"Illegal day of the month: %s\n", - 'first_time_not_tty'=>"To set up your calendar, do the command ``when'' in an interactive terminal window.\n", - 'ask_if_set_up'=>("You can now set up your calendar. This involves creating a directory ~/.when\n". - "(or somewhere else as set by an environment variable, see documentation), and making\n". - "a couple of files in it. If you want to do this, type y and hit return.\n"), - 'error_creating_dir'=>"Error creating the directory %s\n", - 'ask_for_editor'=>("You can edit your calendar file using your favorite editor. Please enter the command you\n" - ."want to use to run your editor, or hit return to accept this default:\n"), - 'error_creating_prefs'=>"Error creating the file %s\n", - 'error_creating_cal'=>"Error creating the file %s\n", - 'getting_started'=>("You can now add items to your calendar file. Do ``when --help'' for more information.\n"), - 'not_unique_w_match'=>'%s does not match a unique day of the week from %s', - 'no_w_match'=>'%s does not match any day of the week from %s', - 'not_valid_expression'=>'%s is not a valid date or expression', - 'illegal_var'=>'illegal variable: %s', - 'illegal_month_in_expression'=>'illegal month in expression: %s', - 'expression_syntax_error'=>'syntax error: %s', - 'error_running_editor'=>"Error executing command %s, %s. Perhaps %s isn't installed?\nYou selected this command in the file %s.\n", - 'describe_julian_day'=>"The date %s corresponds to modified julian day %d.\n", - ) - } - if ($what eq '__give_all_strings') {return values %strings} # used by UnicodeTools::collect_all_strings() - if (exists $strings{$what}) {$what = $strings{$what}} else {$what = w([$what,'en'],@stuff)} # fall back to English - return sprintf($what,@stuff); -} - - -#---------------------------------------------------------------- -# Some constants for ANSI terminal styling. -#---------------------------------------------------------------- -our %ansi_terminal_styling = ( - 'bold'=>1, 'underlined'=>4,'flashing'=>5, - 'fgblack'=>30,'fgred'=>31,'fggreen'=>32,'fgyellow'=>33,'fgblue'=>34,'fgpurple'=>35,'fgcyan'=>36,'fgwhite'=>37, - 'bgblack'=>40,'bgred'=>41,'bggreen'=>42,'bgyellow'=>43,'bgblue'=>44,'bgpurple'=>45,'bgcyan'=>46,'bgwhite'=>47, -); - - -#---------------------------------------------------------------- -# Read the preferences file. -#---------------------------------------------------------------- -sub find_config_dir() { - my @locations = (); # follow XDG standard, https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html - if ($ENV{WHEN_CONFIG_HOME}) { push @locations,$ENV{WHEN_CONFIG_HOME} } - if ($ENV{XDG_CONFIG_HOME}) { push @locations,"$ENV{XDG_CONFIG_HOME}/when" } - push @locations,glob "~/.config/when"; - push @locations,glob "~/.when"; - foreach my $loc(@locations) { - if (-d $loc) { return $loc } - } - return $locations[-1]; # doesn't exist, but it's the typical location, so return it and expect an error -} - -my $dir = find_config_dir(); -my $prefs_file = glob "$dir/preferences"; -our $quickie = 0; -our $got_command_line_options = 0; - -our $explicitly_set_future = 0; - # ...This is set in two places, and used in one place -- see there for an explanation of what it's for. - -if (! -e $prefs_file) { - # Normally we read command-line options after reading the prefs file, to - # allow them to override the file. However, if the prefs file doesn't exist, - # we want to do that now, and find out if this is a run where all we need to - # do is a --version or something. The reason for this complication is that we - # need to handle the case where the root user (who doesn't want to set up his - # own calendar file) is installing when, and when is being run from inside the - # makefile in order to find out what version it is. - my $old_future = $preferences{'future'}; - GetOptions(%command_line_options); # from Getops::Long - my $new_future = $preferences{'future'}; - $explicitly_set_future ||= ($old_future != $new_future); - $got_command_line_options = 1; - $quickie = - ($options{'version'} || $options{'bare_version'} || $options{'make_filter_regex'} || $options{'test_accent_filtering'} || $options{'help'} || $options{'test_expressions'}); -} - -# Note that $ENV{LANG} won't exist on non-Linux systems (e.g., doesn't exist on BSD). Debian -# systems have it set to, e.g., "en_US", but apparently Red Hat does something like "en_US.utf8". -if (exists $ENV{LANG}) { - if ($ENV{LANG} =~ m/^(..)/) { - my $l = lc($1); - $preferences{'language'} = $l; - } -} -# Later on, we check whether the language has been set in the preferences -# file or in a command line option. If the end result is that the language -# is set to something goofy (e.g., because we failed to parse $LANG correctly, -# or because the user's language isn't one we support), then the language defaults -# back to English anyway. - -if (!$quickie && (! -e $dir or ! -e $prefs_file) && !$preferences{'test_mode'}) { - run_first_time($preferences{'calendar'}); -} -if (-e $prefs_file) { - UnicodeTools::file_is_valid_utf8($prefs_file) or die w('not_utf8',$prefs_file); - open (FILE,"<$prefs_file") or die w('error_opening_prefs',$prefs_file); - while (my $line = <FILE>) { - if ($line =~ m/^\s*(\w+)\s*\=\s*([^\s].*)*/) { - my ($option,$value) = ($1,$2); - $value =~ s/#.*$//; # strip comment marked by # - $value =~ s/\s+$//; # strip trailing blanks - $preferences{lc($option)} = $value; - if (lc($option) eq 'future') {$explicitly_set_future=1} - } - else { - if (!($line =~ m/^\s*(#.*)?$/)) {die w('syntax_err_in_prefs',$prefs_file,$line)} - # The (#.*)? is to allow lines consisting of only a comment prefaced by #. - } - } - close FILE; -} -else { - do_output(w('prefs_file_not_found',$prefs_file)) if !($quickie || $preferences{'test_mode'}); -} - -#---------------------------------------------------------------- -# Some global variables. -#---------------------------------------------------------------- -our $date_delimiter = ' '; # e.g. 2003 Feb 1 -our $use_month_names = 1; - -our @month_length = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); - -#---------------------------------------------------------------- -# Figure out what the user wants to do. -#---------------------------------------------------------------- - -my $cmds = ''; - -foreach my $arg(@ARGV) { - if (! ($arg =~ m/^\-/)) { - $cmds = $cmds . lc($arg); - } -} - -if (!$got_command_line_options) { - my $old_future = $preferences{'future'}; - GetOptions(%command_line_options); # from Getops::Long - my $new_future = $preferences{'future'}; - $explicitly_set_future ||= ($old_future != $new_future); -} - -if ($preferences{'rows_auto'} && -t STDOUT) { - $preferences{'rows'} = Terminal::rows(); -} -if ($explicitly_set_future) { - $preferences{'rows'} = 9999; # They explicitly set number of days into future, so don't chop it off. -} - -my $want_styling = ($preferences{'styled_output'} && -t STDOUT) || ($preferences{'styled_output_if_not_tty'} && ! -t STDOUT); - -my @do_what = (); - -if ($cmds eq '') {$cmds = 'i'} - -my %periods = ('w'=>7,'m'=>31,'y'=>366); -#loop over chars in arg: -while ($cmds =~ m/(.)/g) { - my $cmd = $1; - my $recognized = 0; - if (exists $periods{$cmd}) { - $recognized = 1; - push @do_what,'normal'; - $preferences{'future'} = $periods{$cmd}; - $preferences{'rows'} = 9999; # If they say they want a year in advance, don't chop it off. - } - else { - if ($cmd eq 'd') { - $recognized = 1; - push @do_what,'date'; - } - if ($cmd eq 'i') { - $recognized = 1; - push @do_what,'normal'; - } - if ($cmd eq 'c') { - $recognized = 1; - push @do_what,'calendar'; - } - if ($cmd eq 'e') { - $recognized = 1; - push @do_what,'edit'; - } - if ($cmd eq 'j') { - $recognized = 1; - push @do_what,'modified_julian_day'; - } - } - if (!$recognized) { - die w('illegal_command',$cmd); - } -} - -# Setting the calendar file after the command-line options have been read -my $file = glob $preferences{'calendar'}; -if (!$quickie && ! -e $file && !$preferences{'test_mode'}) { - run_first_time($file); -} - -#---------------------------------------------------------------- -# Do it. -#---------------------------------------------------------------- - -my $now = When::current_date(); -if ($preferences{'now'} ne '') { - my $r = When::parse_blank_delimited($preferences{'now'}); - if ($r->[0]) { - die $r->[0]; - } - $now = $r->[1]; -} - -if ($options{'help'}) { - print documentation(); - exit(0); -} - -if ($preferences{'test_expression'}) { - $preferences{'test_expression'} =~ m/^([^,]+),([^,]+),([^,]+),(.*)$/; - my ($date,$result,$expr,$comment) = ($1,$2,$3,$4); - # result is 0, 1, or e if an error is expected - my $expect_err = ($result=~m/e/i); - my $failure = sub { - my $info = shift; - print "Failed test\n $comment\n"; - print " date=$date\n expect_err=$expect_err\n result=$result\n expr=$expr\n"; - print "$info\n"; - exit(-1); - }; - my $match = DateMatch->new('condition',$expr); - if ($match->{ERR} && ! $expect_err) { - my $err = $match->{ERR}; - $failure->("unexpected error, $err"); - } - if ((!($match->{ERR})) && $expect_err) { - $failure->("error expected, but none occurred"); - } - if (!($match->{ERR})) { - my $x = When::parse_blank_delimited($date); - my $err = $x->[0]; - $failure->("error parsing date $date, $err") if $err; - my $when = $x->[1]; - my $got_result = $match->evaluate($when,{},$when->day_of_week); - $failure->("expected result '$result', got '$got_result'") if ($got_result xor $result); - } - exit(0); -} - -if ($options{'version'}) { - print "When version $version, (c) 2003-2011 Benjamin Crowell.\nDo 'when --help' for help and copyleft information.\n"; - exit(0); -} -if ($options{'bare_version'}) { - print $version; - exit(0); -} -if ($options{'make_filter_regex'}) { - print UnicodeTools::make_filter_regex()."\n"; - exit(0); -} -if ($options{'test_accent_filtering'}) { - my @lingos = keys %month_name; - my @strings = (); - foreach my $lingo(@lingos) { - @strings = (@strings,$month_name{$lingo},$month_name_long{$lingo},$wday_name{$lingo}); - my $save = $preferences{'language'}; - $preferences{'language'} = $lingo; - @strings = (@strings,w('__give_all_strings')); - $preferences{'language'} = $save; - } - my $result = UnicodeTools::test_accent_filtering(@strings); # All this does is make sure it ends up pure ascii. - if ($result eq '') { - exit(0); - } - else { - print STDERR $result; - exit(-1); - } -} - -my $first_one = 1; -foreach my $cmd(@do_what) { - if (!$first_one) { - do_output("\n"); - } - $first_one = 0; - if ($cmd eq 'date') { - do_output(describe_date()."\n"); - } - if ($cmd eq 'normal') { - if (normal_behavior($now,$want_styling)!=0) {exit(-1)}; - } - if ($cmd eq 'calendar') { - calendar(NOW=>$now,WANT_STYLING=>$want_styling,TODAY_STYLE=>$preferences{'calendar_today_style'},PAST=>$preferences{'past'},FUTURE=>$preferences{'future'}); - } - if ($cmd eq 'edit') { - my $c = $preferences{'editor'}." ".$preferences{'calendar'}; - unless(system($c)==0) { - print STDERR w('error_running_editor',$c,$!,$preferences{editor},$prefs_file); - exit(-1); - } - } - if ($cmd eq 'modified_julian_day') { - #print "The date ".$now->string_human()." corresponds to modified julian day ".$now->modified_julian_day().".\n"; - print w('describe_julian_day',$now->string_human(),$now->modified_julian_day()); - } -} - -sub describe_date { - my $describe_wday = $now->day_of_week_name; - my $describe_today = $now->string_human(); - my @tm = localtime; - my ($hour,$minute) = ($tm[2],$tm[1]); - $hour = (($hour-1) % 12)+1 if ($preferences{'ampm'}); - my $describe_time = sprintf "%d:%02d",$hour,$minute; - return "$describe_wday $describe_today $describe_time"; -} - -sub do_output { - my $x = shift; - print filter_accents_if_desired($x); -} - -sub do_output_to_stderr { - my $x = shift; - print STDERR filter_accents_if_desired($x); -} - -sub filter_accents_if_desired { - my $x = shift; - if ($preferences{'filter_accents_on_output'}) { - $x = UnicodeTools::filter_out_accents($x); - } - return $x; -} - -#---------------------------------------------------------------- -# The program's normal behavior is to print out all your appointments -# for a certain period (by default, the next two weeks). -#---------------------------------------------------------------- - -sub normal_behavior { - my $now = shift->clone; - my $want_styling = shift; - - my $complete_output = ''; - my $lines_done = 2; # header and blank line under it - - if ($preferences{'header'}) { - $complete_output = $complete_output . filter_accents_if_desired(describe_date())."\n\n"; - $lines_done += 2; # header and blank line under it - } - - my @find_longest = (w('yesterday'),w('today'),w('tomorrow')); - for (my $day_num=1; $day_num<=7; $day_num++) { - push @find_longest,When::short_wday_name($day_num); - } - my $max_wday_length = 8; - foreach my $day(@find_longest) { - my $length = AnsiTerminalStyling::length(filter_accents_if_desired($day)); # filter_accents_if_desired may change length of string - $max_wday_length = $length if $length>$max_wday_length; - } - - -r $file or die w('error_opening_calendar',$file); - - # Make sure the calendar file is utf8. Horrible things happen if, e.g., it's iso-8859. - # This involves reading the file again. We do this after the original reading of the file, - # since that's where the error handling is if the file doesn't exist, etc. - UnicodeTools::file_is_valid_utf8($file) or die w('not_utf8',$file); - - my $prefilter = $preferences{'prefilter'}; - my $calendar_input; - if ($prefilter eq '') { - $calendar_input = "<$file"; - } - else { - $calendar_input = "$prefilter <$file |"; - } - - open (FILE, $calendar_input) or die w('error_opening_calendar',$file); - my @lines = <FILE>; # in array context, returns all lines from file - close FILE or die w("error_prefiltering",$calendar_input); - - my @show; - my @condition_lines; - - my $list_one = sub { - my $when = shift; - my $delta = shift; - my $what = shift; - my $describe = $when->day_of_week_name; - if ($delta == -1) {$describe = w('yesterday')} - if ($delta == 0) {$describe = AnsiTerminalStyling::style_text(w('today'),$preferences{'items_today_style'},$want_styling)} - if ($delta == 1) {$describe = w('tomorrow')} - $describe = filter_accents_if_desired($describe); - $describe = AnsiTerminalStyling::pad_to_desired_length($describe,$max_wday_length+1,' '); - my $say = sprintf "%s %s %s\n",$describe,$when->string_human,$what; - push @show,[$when->clone,$say,$what]; - }; - foreach my $line(@lines) { - chomp $line; - if ($line =~ m/^\s*([^,#]*)\s*,\s*(([^\s].*)?)/) { - my ($date,$what) = ($1,$3); - my $match; - my $exact = !($date=~m/[=<>%]/); # exact includes both dates of the form "2008 jul 4" and those like "2008* jul 4" and "* jul 4" - my $literal = $exact && !($date=~/\*/); # literal means only dates of the form "2008 jul 4" - my $only_if_literal = $preferences{literal_only}; - my $ignore = (!$literal) && $only_if_literal; - if (!$ignore) { - if ($exact) { - $match = DateMatch->new('exact',$date); - if ($match->{ERR} ne '') { - my $err = $match->{ERR}; - do_output_to_stderr("****** $err\n****** $line\n\n"); - return -1; - } - } - else { - $match = DateMatch->new('condition',$date); - if ($match->{ERR}) { - my $err = $match->{ERR}; - do_output_to_stderr("****** $err\n****** $line\n\n"); - return -1; - } - } - if ($match->{TYPE} eq 'condition') { - push @condition_lines,[$match,$what]; - } - if ($match->{TYPE} eq 'exact') { - my $when = $match->{WHEN}; - if ($when->y =~ m/\*/) { - my $that_year = ''; - if ($when->y =~ m/(\d+)/) {$that_year=$1}; # "1996*" syntax - $when->y($now->y); - if ($when->delta_days($now)<$preferences{'past'}) {$when->y($when->y+1)} - if ($when->delta_days($now)>$preferences{'future'}) {$when->y($when->y-1)} - if ($when->y =~ m/(\d+)/) { - my $years_since = $when->y()-$that_year; - $what =~ s/\\a/$years_since/g; - $what =~ s/\\y/$that_year/g; - } - } - my $delta = $when->delta_days($now); - if ($delta >= $preferences{'past'} && $delta <=$preferences{'future'}) { - &$list_one($when,$delta,$what); - } - } - } # end if not ignored - } # end if line has the syntax of a calendar entry - else { # Ignore lines consisting only of whitespace and lines that begin with # (comments). - if (!($line =~ m/^\s*$/ || $line =~ m/^#/)) {die w('syntax_error_in_calendar',$line)} - } - } - - if (@condition_lines) { - my $then = $now->clone; - my ($d1,$d2) = ($preferences{'past'},$preferences{'future'}); - $then->add_delta_days_in_place($d1); - my $day_of_week = $then->day_of_week; - for (my $delta=$d1; $delta<=$d2; $delta++) { - my %vars = (); - foreach my $cond(@condition_lines) { - my ($match,$what) = @$cond; - my $result = $match->evaluate($then,\%vars,$day_of_week); - &$list_one($then,$delta,$what) if $result; - } # end loop over condition lines - $then->increment_day_in_place(); - $day_of_week = ($day_of_week%7)+1; # we go 1..7, not 0..6; this is like ((x-1+1)%7)+1 - } # end loop over days - } # end if condition lines - - # For purposes of sorting entries, we recognize when the text of an entry begins with a time in h:mm format, with an optional a or p for am or pm. - # This subroutine returns 9999 if the string doesn't begin with a time, or the time in minutes otherwise. - my $get_time = sub { - my $x = shift; - my $match_ampm = $preferences{'ampm'} ? '[ap]?' : ''; - return 9999 unless $x =~ /^\s*(\d+):(\d+)($match_ampm)/i; - my ($h,$m,$am_pm) = ($1,$2,$3); - if (!$am_pm && $h<$preferences{'auto_pm'}) {$h+=12} - if ($am_pm=~/p/ && $h != 12 ) {$h+=12} - if ($am_pm=~/a/ && $h == 12) { $h-=12 } - return 9999 unless ($h<=23 && $m<=59); - return $h*60+$m; - }; - @show = sort { - my $time_order = $a->[0]->compare($b->[0]); - return $time_order if $time_order; - my ($txt_a,$txt_b) = ($a->[2],$b->[2]); # the texts of the two calendar entries - &$get_time($txt_a) <=> &$get_time($txt_b) - # In the case where they're both times, this makes the earlier one come first. - # In the case where one is a time and one isn't, the timed one comes first. - # In the case where neither is timed, this also does the right thing. - } @show; - - my $wrap = $preferences{'wrap'}; - if ($wrap==0) {$wrap=9999} - if ($preferences{'wrap_auto'}) { - my $terminal_width = Terminal::columns(); - if ($terminal_width>0) {$wrap=$terminal_width} - } - if ($preferences{'wrap_max'}>0 && $wrap>$preferences{'wrap_max'}) { - $wrap=$preferences{'wrap_max'} - } - my $rows = $preferences{'rows'}; - if ($rows==0) {$rows=9999} - my $margin = $max_wday_length+14; # This is the width of the column containing the date. - if ($wrap<$margin+10) {$wrap=$margin+10} # otherwise you get an endless loop - foreach my $thing(@show) { - my ($when,$say) = @$thing; - my $output = format_item($say,$wrap,$margin); - $lines_done += split /\n/,$output; - last if $lines_done>$rows && $when->delta_days($now)>3; - $complete_output = $complete_output . filter_accents_if_desired($output); - } - my $did_it = 0; - if ($preferences{'paging'} && -t STDOUT && Terminal::rows()>0 && $lines_done>Terminal::rows()-1) { - my $pager = $ENV{PAGER}; - if (!defined $pager) {$pager = "less"} - if ($pager eq 'less') {$pager = "$pager ".$preferences{'paging_less_options'}} - if (open(FILE,"| $pager")) { - print FILE $complete_output; - close FILE; - $did_it = 1; - } - } - if (!$did_it) {print $complete_output} - return 0; -} - -#---------------------------------------------------------------- -# Print a calendar. -#---------------------------------------------------------------- -sub calendar { - my %args = ( - NOW=>'', - WANT_STYLING=>0, - TODAY_STYLE=>'', - PAST=>0, - FUTURE=>0, - @_, - ); - - my $now = $args{NOW}->clone; - my $past = $args{PAST}; - my $future = $args{FUTURE}; - - # Normally we just print out three months, next to each other horizontally. - # However, if the user specified some other time period than the default, we - # print out multiple rows, e.g., they may want a full year's worth of calendars (four lines). - # The following are generous limits -- we check more carefully later: - my $row_start = int($past/90)-2; - my $row_end = int($future/90)+2; - - for (my $row=$row_start; $row<=$row_end; $row++) { - - my $middle_month = $now->clone; - for (my $i=1; $i<=abs($row); $i++) { - if ($row<0) { - $middle_month->decrement_month_in_place(); - $middle_month->decrement_month_in_place(); - $middle_month->decrement_month_in_place(); - } - if ($row>0) { - $middle_month->increment_month_in_place(); - $middle_month->increment_month_in_place(); - $middle_month->increment_month_in_place(); - } - } - my $last_month = $middle_month->clone; - my $next_month = $middle_month->clone; - $last_month->decrement_month_in_place(); - $next_month->increment_month_in_place(); - - # Check whether we really need to print this row in order to cover their chosen time period: - my $range_lo = $last_month->clone; - my $range_hi = $next_month->clone; - $range_lo->d(1); - $range_hi->d($range_hi->days_in_month); - my $needed = $range_hi->delta_days($now)-$past>=0 && $range_lo->delta_days($now)-$future<=0; - - if ($needed) { - my @cals; - for (my $i=-1; $i<=1; $i++) { - my $month = [$last_month,$middle_month,$next_month]->[$i+1]; - my @cal = make_one_month_calendar(WHEN=>$month,WANT_STYLING=>$args{WANT_STYLING},TODAY_STYLE=>$args{TODAY_STYLE},MARK_TODAY=>($i==0 && $row==0)); - my $month_needed = 1; - if (!$preferences{'neighboring_months'}) { - my $first_day_of_month = $month->clone; - $first_day_of_month->d(1); - my $last_day_of_month = $month->clone; - $last_day_of_month->d($month->days_in_month); - $month_needed=0 if $first_day_of_month->delta_days($now)-$future>0 || $last_day_of_month->delta_days($now)-$past<0; - } - push @cals,\@cal if $month_needed; - } - do_output(combine_calendars_for_several_months(@cals)); - } # end if $needed - - } # end loop over $row - -} - -sub combine_calendars_for_several_months { - my @cals = @_; - my $output = ''; - for (my $line_num=0; $line_num<=10; $line_num++) { - my $have_one = 0; - foreach my $cal(@cals) { - $have_one = $have_one || exists $cal->[$line_num]; - } - if ($have_one) { - my $full_line = ''; - foreach my $cal(@cals) { - my $part; - if (exists $cal->[$line_num]) { - $part = $cal->[$line_num]; - } - else { - $part = (' ' x 21); #### constant shouldn't be hardcoded - } - if ($full_line ne '') {$full_line = "$full_line "} - $full_line = $full_line . $part; - } - $output = $output . $full_line . "\n"; - } - else { - last - } - } # end loop over $line_num - return $output; -} - -sub make_one_month_calendar { - my %args = ( - MARK_TODAY=>0, - WANT_STYLING=>0, - TODAY_STYLE=>'', - @_, - ); - my $when = $args{WHEN}->clone; - my $mark_today = $args{MARK_TODAY}; - my @cal = (); - - my $today = $when->clone; - - my $columns = 21; # 3 columns for each day of the week - - my $month_name_long = When::month_name_long($when->m); - # Pad it on the front and back so it's roughly centered: - my $pad_character = '-'; - while (AnsiTerminalStyling::length($month_name_long)<$columns) { - if (AnsiTerminalStyling::length($month_name_long)%2==0) { - $month_name_long = "$month_name_long$pad_character"; - } - else { - $month_name_long = "$pad_character$month_name_long"; - } - } - push @cal,$month_name_long if $preferences{'header'}; - - my $line = ''; - for (my $wday=1; $wday<=7; $wday++) { - $line = $line . ' '.When::short_wday_name($wday).' '; - } - push @cal,$line if $preferences{'header'}; - - $line = ''; - $when->d(1); - for (my $wday=1; $wday<$when->day_of_week && $wday<=7; $wday++) { - $line = $line . ' '; - } - my $this_month = $when->m; - while ($when->m == $this_month) { - my $display = sprintf('%2d',$when->d); - if ($when->compare($today)==0 && $mark_today) { - if ($args{WANT_STYLING} && $args{TODAY_STYLE} ne '') { - $display = AnsiTerminalStyling::style_text($display,$args{TODAY_STYLE},$args{WANT_STYLING}); - } - else { - $display = ' *'; - } - } - $line = "$line$display "; - $when->increment_day_in_place(); - # The first day of a second and the subsequent weeks in this month - # will be printed on the next line in the calendar - if ($when->day_of_week==1) {push @cal,$line; $line = ''} - } - if ($line ne '') {push @cal,$line; $line = ''} - - # Make sure all the lines are equal in length: - for (my $i=0; $i<=$#cal; $i++) { - my $line = $cal[$i]; - while (AnsiTerminalStyling::length($line)<$columns) { - $line = "$line "; - } - $cal[$i] = $line; - } - - return @cal; -} - - -#---------------------------------------------------------------- -# Help them set up the first time through. -#---------------------------------------------------------------- -sub run_first_time { - my $cal_file = glob shift; - - if (!(-t STDIN && -t STDOUT)) { - die w('first_time_not_tty'); - } - - print w('ask_if_set_up'); - my $want_to = <STDIN>; - chomp $want_to; - if (lc($want_to) ne 'y') {exit(-1)} - - my $dir = glob "~/.when"; - if (! -d $dir) { - mkdir($dir) or die sprintf w('error_creating_dir'),$dir; - } - - my $editor = $preferences{'editor'}; - print w('ask_for_editor'); - print " $editor\n"; - my $want_editor = <STDIN>; - chomp $want_editor; - if ($want_editor ne '') {$editor=$want_editor} - - my $prefs = glob "~/.when/preferences"; - if (! -e $prefs) { - open(PREFS,">$prefs") or die sprintf w('error_creating_prefs'),$prefs; - print PREFS "calendar = $cal_file\n"; - print PREFS "editor = $editor\n"; - close(PREFS); - } - if (! -e $cal_file) { - open(CAL,">$cal_file") or die w('error_creating_cal'),$cal_file; - close(CAL); - } - - print w('getting_started'); -} - -#---------------------------------------------------------------- -# Routines for wrapping long lines: -#---------------------------------------------------------------- - -# Format a line of text for output. Wrap long lines nicely. -sub format_item { - my $text = shift; - my $wrap = shift; - my $margin = shift; - chomp $text; - my @stuff = split_a_line($text,$wrap,2); - my $result = (shift @stuff)."\n"; - my @the_rest = (); - if (@stuff) { - @the_rest = split_a_line((shift @stuff),$wrap-$margin,9999); - } - foreach my $line(@the_rest) { - $result = $result . (' ' x $margin) . "$line\n"; - } - return $result; -} - -# Split a long line into shorter pieces, preferably at word breaks. Do unicode->ascii -# filtering here (if it's turned on), because sometimes filtering turns a single -# unicode character to two ascii characters (e.g., Danish o_slash => oe). -sub split_a_line { - my $text = shift; - my $width = shift; - my $max_pieces = shift; - if ($preferences{'filter_accents_on_output'}) { - $text = UnicodeTools::filter_out_accents($text); # see comment above - } - if (AnsiTerminalStyling::length($text)<=$width || $max_pieces==1) {return ($text,)} - if ($text =~ m/^(.{0,$width})(\s+(.*))?$/) { - my ($a,$b) = ($1,$3); - return ($a,split_a_line($b,$width,$max_pieces-1)) - } - # The only reason we'd get to this point is if the input starts with an extremely long line - # consisting of one extremely long word with no blanks in it. This means we can't - # break at a word boundary. This typically happens with URLs. - $text =~ m/^(.{0,$width})(.*)$/; - my ($a,$b) = ($1,$2); - return ($a,split_a_line($b,$width,$max_pieces-1)); -} - - -#---------------------------------------------------------------- -# A DateMatch object knows how to test whether a given date -# matches it or not. -#---------------------------------------------------------------- -package DateMatch; - -# Error handling: ERR field is set to a null string or a string containing a fully processed, internationalized error message -sub new { - my $class = shift; - my $self = {}; - bless($self,$class); - $self->{TYPE} = shift; # can be 'exact' or 'condition' - $self->{ERR} = undef; - my $source = shift; - if ($self->{TYPE} eq 'exact') { - my $parsed_date = When::parse_blank_delimited($source); - my ($err,$when) = @$parsed_date; - $self->{WHEN} = $when; - $err =~ s/\n$//; - $self->{ERR} = $err; - return $self; - } - if ($self->{TYPE} eq 'condition') { - $self->{SOURCE} = $source; - my $expr = compile_expression($source); - my $e = $expr->[0]; - if (!defined $e) {$e = ''} - if (ref $e) {$e = main::w(@$e)} - $self->{ERR} = $e; - $self->{PARSED} = $expr->[1]; - return $self; - } # end if it's a condition -} - -sub evaluate { - my $self = shift; - my $when = shift; - my $vars = shift; # for efficiency; avoid recalculating these if possible - my $day_of_week = shift; # for efficiency, must be provided by caller - my @stack = (); - my $parsed = $self->{PARSED}; - foreach my $rpn (@$parsed) { - #---- push variable onto stack - if ($rpn =~ m/^[a-z]$/) { - if ($rpn =~ m/^[abdjmywcnez]$/) { - if (!exists $vars->{$rpn}) { - # parser should have caught it if it wasn't one of these vars: - if ($rpn eq 'w') {$vars->{$rpn}=$day_of_week} - if ($rpn eq 'y') {$vars->{$rpn}=$when->y} - if ($rpn eq 'm') {$vars->{$rpn}=$when->m} - if ($rpn eq 'd') {$vars->{$rpn}=$when->d} - if ($rpn eq 'j') {$vars->{$rpn}=$when->modified_julian_day} - if ($rpn eq 'a') {$vars->{$rpn}=$when->week_a} - if ($rpn eq 'b') {$vars->{$rpn}=$when->week_b} - if ($rpn eq 'c') {$vars->{$rpn}=$when->adjacent_weekend_day} - if ($rpn eq 'n') {$vars->{$rpn}=$when->days_in_month} - if ($rpn eq 'e') {$vars->{$rpn}=Easter::easter($when->y)->delta_days($when)} - if ($rpn eq 'z') {$vars->{$rpn}=$when->day_of_year} - } - push @stack,$vars->{$rpn}; - } - else { - die "Illegal variable $rpn in expression @$parsed"; # kludge, not internationalized; parser should have caught it before this anyway - } - } - #---- push constant onto stack - elsif ($rpn =~ m/^\d+$/) { push @stack, $rpn } - #---- unary op: ! - elsif ($rpn eq '!') { - my $a = pop @stack; - push @stack, !$a; - } - #---- binary op: =, <, etc. - else { - my $b = pop @stack; - my $a = pop @stack; - # the one that came first in the source is second to come off the stack - my $result; - if ($rpn eq '=') { $result = $a == $b } - elsif ($rpn eq '<') { $result = $a < $b } - elsif ($rpn eq '>') { $result = $a > $b } - elsif ($rpn eq '<=') { $result = $a <= $b } - elsif ($rpn eq '>=') { $result = $a >= $b } - elsif ($rpn eq '!=') { $result = $a != $b } - elsif ($rpn eq '%') { $result = $a % $b } - elsif ($rpn eq '-') { $result = $a - $b } - elsif ($rpn eq '&') { $result = $a && $b } - elsif ($rpn eq '|') { $result = $a || $b } - push @stack, $result; - } - } # end loop over RPN - return pop @stack; -} - -sub priority($) { - my $op = shift; - #if ($op eq '!') { return 1 } - if ($op eq '%') { return 2 } - if ($op eq '-') { return 3 } - if ($op eq '>' || $op eq '<' || $op eq '<=' || $op eq '>=') { return 4 } - if ($op eq '=' || $op eq '!=') { return 5 } - if ($op eq '!') { return 6 } - if ($op eq '&') { return 7 } - if ($op eq '|') { return 8 } - if ($op eq '(') { return 9 } - return 0; -} - -# Error handling: first element of return value is either undef or an array to be passed to w() for internationalization. -sub compile_expression { # a test such as 'm=dec & d=25' - my $source = lc shift; - my @ex = split / */, $source; # split into individual characters, stripping whitespace - my @rpn; - my @opst = ('('); # bottom for the stack - my ($op, $op2, $pr, $err); - - my $i = 0; - while ($i < @ex) { - my $single_alpha_token = ($ex[$i] =~ /[[:alpha:]]/ && $ex[$i+1] !~ /[[:alpha:]]/); # this char is alphabetic but the next char is not, i.e., we're looking at a one-character token - my $multiple_alpha_token = ($ex[$i] =~ /[[:alpha:]]/ && $ex[$i+1] =~ /[[:alpha:]]/); - my $left = $rpn[-1]; - my $testing_equality = $i>0 && $ex[$i-1] =~ /^[=<>]$/; - my $expect_literal = ($left=~/^[mw]$/) && $testing_equality && $ex[$i] ne '='; # we're parsing the right-hand-side of something like m=jan or w=thu - if ($ex[$i] =~ /\d/) { - my $num = 0; - while ($i < @ex && $ex[$i] =~ /\d/) { - $num = 10 * $num + $ex[$i]; - $i++; - } - push @rpn, $num; - } - elsif ($single_alpha_token && !$expect_literal) { - if ($ex[$i] =~ /[abcdjmnywez]/) { push @rpn, $ex[$i++] } - else {$err = ['illegal_var', $ex[$i++]] } - } - elsif ($multiple_alpha_token || $expect_literal) { # this is or should be a month or weekday literal like jan or thu - if ($multiple_alpha_token && !$expect_literal) {return [['expression_syntax_error',$source],undef]} # literals like jan or thu can only appear on r.h.s. of m= or w= - my $lvar; - while ($i < @ex && $ex[$i] =~ /[[:alpha:]]/) { - $lvar .= $ex[$i]; - $i++; - } - # $left is guaranteed to be m or w at this point - if ($left eq 'm') { - my $parsed_month = When::parse_month_name($lvar); - if (!$parsed_month) {$err = ['illegal_month_in_expression',$lvar]} - $lvar = $parsed_month; - } - if ($left eq 'w') { - my $r = When::parse_wday_name($lvar); - if ($r->{'err'}) { - $err = [$r->{'err'},$lvar, $wday_name{$preferences{'language'}}]; - } - $lvar = $r->{'match'}; - } - push @rpn, $lvar; # $lvar may be null in cases like w=t, where t is ambiguous and causes an error - } - elsif ($ex[$i] eq '(') { - push @opst, '('; - $i++; - } - elsif ($ex[$i] eq ')') { - $op = pop @opst; - while ($op ne '(') { - push @rpn, $op; - $op = pop @opst; - } - $i++; - } - elsif ($ex[$i] eq '!' && $ex[$i+1] ne '=') { - # '!' has right associativity, so it is in a separate case - $pr = priority '!'; - $op2 = pop @opst; - while ($pr > priority $op2) { - push @rpn, $op2; - $op2 = pop @opst; - } - push @opst, $op2; - push @opst, '!'; - $i++; - } - elsif ($ex[$i] =~ /[!%\-\&\|<>=]/) { - $op = $ex[$i]; - if ($op =~ /[\!<>]/ && $ex[$i+1] eq '=') { - $op .= '='; - $i++; - } - $pr = priority $op; - $op2 = pop @opst; - while ($pr >= priority $op2) { - push @rpn, $op2; - $op2 = pop @opst; - } - push @opst, $op2; - push @opst, $op; - $i++; - } - else { return [['not_valid_expression',$source]] } - } - while ($op = pop @opst and $op ne '(') { push @rpn, $op } - if (@opst) { $err = ['not_valid_expression',$source] } - return [$err, \@rpn]; -} - -#---------------------------------------------------------------- -# A When object stores year, month, day, time (ymdt). -# Month is 1..12 -# Time is null, or hour, or hour:min; hour is on 24-hour time. -# There are methods for doing calculations with the Gregorian calendar. -#---------------------------------------------------------------- - -package When; - - -sub new { - my $class = shift; - my $self = {}; - bless($self,$class); - $self->{Y} = shift; - $self->{M} = shift; - $self->{D} = shift; - if (@_) { - $self->{T}=shift - } - else { - $self->{T} = ''; - } - return $self; -} - -# Returns [e,w], where w is a When object and e is a null string or a fully processed, internationalized error message -sub parse_blank_delimited { - my $date = shift; - chomp $date; - my @a = split / +/,$date; - if ($date=~/\*.*\*/) {return [main::w('multiple_wildcards_in_date',$date)]} - if ($#a+1!=3) {return [main::w('date_syntax_error',$date)]} # should have exactly three parts, y m d - if ($a[0] ne '*' && ($a[0]<1900 || $a[0]>2200)) {return [main::w('illegal_year',$a[0])]} - $a[1] = parse_month_name($a[1]); - if ($a[1] eq '' || $a[1]<1 || $a[1]>12) {return [main::w('illegal_month',$a[1])]} - my $w = When->new(@a); - my $len = $w->days_in_month; # returns 29 if input is * feb 29 - if ($a[2]<1 || $a[2]>$len) {return [main::w('illegal_day_of_month',$date)]} - return ['',$w]; -} - -# can be number or name -# if it's a name, ignores case, trailing dot, and extra characters not needed for uniqueness -BEGIN { -my %cache = (); # cache results, because this routine tends to be a cpu hog -sub parse_month_name { - my $name = shift; - my $orig_name = $name; - if ($name =~ m/\d+/) {return $name} - return $cache{$name} if exists $cache{$name}; - $name = UnicodeTools::filter_out_accents($name); - $name =~ s/\.$//; # remove trailing dot - my $language = $preferences{'language'}; - # Special case for czech, where Cerven/Cervenec tie us up in knots: - if ($language eq 'cs') { - return 6 if lc($name) eq 'cer'; - return 7 if lc($name) eq 'cec'; - } - my @try_langs = (); - push @try_langs,$language if exists $month_name{$language}; - if ($language ne 'en') { - push @try_langs,'en'; - } - my %matches = (); - foreach my $try_lang(@try_langs) { - for (my $m=1; $m<=12; $m++) { - my $n = UnicodeTools::filter_out_accents(month_name_long($m,$try_lang)); - if ($name =~ m/^$n/i || $n =~ m/^$name/i) {$matches{$m}=1} - } - my @matches = keys %matches; - if (@matches==1) {my $result = $matches[0]; $cache{$orig_name}=$result; return $result} - if (@matches>1) {return ''} # ambiguous - } - return ''; -} -} - -# ignores case, trailing dot, and extra characters not needed for uniqueness -sub parse_wday_name { - my $name = shift; - $name = UnicodeTools::filter_out_accents($name); - $name =~ s/\.$//; # remove trailing dot - my $language = $preferences{'language'}; - my @try_langs = (); - push @try_langs,$language if exists $wday_name{$language}; - if ($language ne 'en' || @try_langs==0) { - push @try_langs,'en'; - } - my @matches = (); - my $n_matches = 0; - foreach my $try_lang(@try_langs) { - for (my $w=1; $w<=7; $w++) { - my $n = UnicodeTools::filter_out_accents(wday_name($w,$try_lang)); - if ($name =~ m/^$n/i || $n =~ m/^$name/i) {push @matches,$w; ++$n_matches;} - } - if ($n_matches==1) { return {'match'=>$matches[0]}} - if ($n_matches>1) { return {'err'=>'not_unique_w_match'} } # ambiguous - } - return {'err'=>'no_w_match'}; -} - -sub clone { - my $self = shift; - return When->new($self->array); -} - -sub array { - my $self = shift; - return ($self->y,$self->m,$self->d,$self->t); -} - -sub y { - my $self = shift; - if (@_) {$self->{Y} = shift} - return $self->{Y}; -} - -sub m { - my $self = shift; - if (@_) {$self->{M} = shift} - return $self->{M}; -} - -sub d { - my $self = shift; - if (@_) {$self->{D} = shift} - return $self->{D}; -} - -sub t { - my $self = shift; - if (@_) {$self->{T} = shift} - return $self->{T}; -} - -sub hour { - my $self = shift; - my $t = $self->t; - $t =~ m/^\d+/; - return $1; -} - -# returns null string if not set -sub min { - my $self = shift; - my $t = $self->t; - if ($t =~ m/\d+\:(\d_)/) { - return $1; - } - else { - return ''; - } -} - -sub min_no_null { - my $self = shift; - my $m = $self->min; - if ($m eq '') {$m=0} - return $m; -} - -sub current_date { - my @tm = localtime; - my $y = $tm[5]; - my $m = $tm[4]+1; - my $d = $tm[3]; - if ($y<1900) {$y=$y+1900} # works in Perl 5 and 6 - return When->new($y,$m,$d); -} - - -sub string_sortable { - my $self = shift; - return sprintf "%04d-%02d-%02d %02d:%02d", $self->y,$self->m,$self->d,$self->hour,$self->min_no_null; -} - -# y,m,d = numbers, m=1..12; t is h or h:m, on 24-hour time -sub string_human { - my $when = shift; - my ($y,$m,$d,$t) = $when->array; - if ($use_month_names) {$m=month_name($m)} - if (length($d)==1) {$d=" $d"} - my $result = $y.$date_delimiter.$m.$date_delimiter.$d; - if ($t ne '') {$result = $result . ' '.time_string_human($t)} - return $result; -} - -sub time_string_human { - my $self = shift; - my ($h,$m) = ($self->hour,$self->min); - my $suffix = ''; - if ($preferences{ampm}) { - if ($h>12) { - $h=$h-12; - $suffix = 'pm' - } - else { - $suffix = 'am'; - } - } - if ($m ne '') { - return sprintf '%d:%02d%s',$h,$m,$suffix; - } - else { - return sprintf '%d%s',$h,$suffix; - } -} - -sub month_name { - my $m = shift; - return month_name_short_or_long($m,'short',@_); -} - -sub month_name_long { - my $m = shift; - return month_name_short_or_long($m,'long',@_); -} - -sub month_name_short_or_long { - my $m = shift; - my $short_or_long = shift; - my $list_of_names; - if ($short_or_long eq 'short') { - $list_of_names = \%month_name; - } - if ($short_or_long eq 'long') { - $list_of_names = \%month_name_long; - } - if (! ref $list_of_names) {return ''} - my $language = $preferences{'language'}; - if (@_) {$language = shift} - if ($m<1 || $m>12) {return ''} - my $names; - if (exists $list_of_names->{$language}) { - $names = $list_of_names->{$language} - } - else { - $names = $list_of_names->{'en'}; - } - my @names = split / /,$names; - return $names[$m-1]; -} - -sub wday_name { - my $d = shift; - my $lang; - if (@_) { - $lang = shift; - } - else { - $lang = $preferences{'language'}; - } - if (!exists $wday_name{$lang}) {$lang='en'} - my $names = $wday_name{$lang}; - if ($d<1 || $d>7) {return ''} - my @names = split / /,$names; - my $offset = $preferences{'monday_first'} ? 1 : 0; - # FIXME -- shouldn't really be referring to this global - return $names[($d-1+$offset)%7]; -} - -sub short_wday_name { - my $d = shift; - if ($d<1 || $d>7) {return ''} - wday_name($d) =~ m/^(.)/; # extract first character - return $1; -} - -sub compare { - my $a = shift; - my $b = shift; - if ($a->y != $b->y) {return $a->y <=> $b->y} - if ($a->m != $b->m) {return $a->m <=> $b->m} - return $a->d <=> $b->d; -} - -sub increment_day_in_place { - my $self = shift; - $self->d($self->d+1); - if ($self->d <= $self->days_in_month()) {return} - $self->m($self->m+1); - $self->d(1); - if ($self->m <= 12) {return} - $self->y($self->y+1); - $self->m(1); -} - -sub add_delta_days_in_place { - my $self = shift; - my $d = shift; - $self->d($self->d+$d); - while ($self->d > $self->days_in_month()) { - $self->d($self->d - $self->days_in_month()); - $self->m($self->m+1); - if ($self->m > 12) { - $self->m(1); - $self->y($self->y+1); - } - } - while ($self->d < 1) { - $self->m($self->m-1); - if ($self->m < 1) { - $self->m(12); - $self->y($self->y-1); - } - $self->d($self->d + $self->days_in_month()); - } -} - -sub increment_month_in_place { - my $self = shift; - $self->m($self->m+1); - if ($self->m > 12) { - $self->y($self->y+1); - $self->m(1); - } - while ($self->d > $self->days_in_month()) { - $self->d($self->d-1) - } -} - -sub decrement_month_in_place { - my $self = shift; - $self->m($self->m-1); - if ($self->m < 1) { - $self->y($self->y-1); - $self->m(12); - } - while ($self->d > $self->days_in_month()) { - $self->d($self->d-1) - } -} - -sub modified_julian_day { - my $self = shift; - return $self->delta_days(When->new(2003,2,14))+52685; -} - -sub day_of_year { - my $self = shift; - return $self->delta_days(When->new($self->y,1,1))+1; -} - -sub week_a { - my $self = shift; - return int((($self->d)-1)/7)+1; -} - -sub week_b { - my $self = shift; - return int(($self->days_in_month()-($self->d))/7)+1; -} - -sub adjacent_weekend_day { - my $self = shift; - my $w = $self->day_of_week(); - if ($w==2) {return ($self->d)-1} - if ($w==6) {return ($self->d)+1} - return -1; -} - -sub delta_days { - my $a = shift; - my $b = shift; - my $compared = $a->compare($b); - if ($compared == 0) {return 0} - if ($compared == -1) {return -($b->delta_days($a))} - if ($a->d != 1 || $b->d !=1) { - my $aa = $a->clone; - my $bb = $b->clone; - $aa->{D} = 1; - $bb->{D} = 1; - return ($aa->delta_days($bb))+($a->d)-($b->d); - } - if ($a->m != 1 || $b->m !=1) { - my $aa = $a->clone; - my $bb = $b->clone; - my $correction = 0; - while ($aa->m > 1) { - $aa->m($aa->m-1); - $correction = $correction + $aa->days_in_month; - } - while ($bb->m > 1) { - $bb->m($bb->m-1); - $correction = $correction - $bb->days_in_month; - } - return ($aa->delta_days($bb))+$correction; - } - # From Jan 1 of one year to Jan 1 of another; $a is after $b - my $aa = $a->clone; - my $result = 0; - while ($aa->y > $b->y) { - $aa->y($aa->y-1); - if ($aa->is_leap_year) { - $result = $result+366; - } - else { - $result = $result+365; - } - } - return $result; -} - -sub days_in_month { - my $self = shift; - if ($self->m != 2) {return $month_length[($self->m)-1]} - if ($self->y eq '*' || $self->is_leap_year) { # I use this routine for error checking; * feb 29 is OK. - return 29; - } - else { - return 28; - } -} - -sub is_leap_year { - my $self = shift; - my $y = $self->y; - if ($y%4!=0) {return 0} - if ($y%100!=0) {return 1} - if ($y%400!=0) {return 0} - return 1; -} - -# Sun=1, ... Sat=7 -sub day_of_week { - my $self =shift; - my $offset = $preferences{'monday_first'} ? -1 : 0; # FIXME -- shouldn't really be referring to this global - return (($self->delta_days(When->new(2003,2,2))+$offset)%7)+1; # Compare against Feb. 2, 2003, which we know was a Sunday. -} - -sub day_of_week_name { - my $self =shift; - return wday_name($self->day_of_week); -} - -#---------------------------------------------------------------- -# ANSI terminal styling -#---------------------------------------------------------------- - -package AnsiTerminalStyling; - -sub style_text { - my $x = shift; - my $style = lc(shift); - my $are_you_sure = shift; - if (!$are_you_sure) {return $x} - my ($before,$after) = ('',''); - while ($style =~ m/([a-z]+)/g) { - my $this_style = $1; - if (exists $ansi_terminal_styling{$this_style}) { - my $code=$ansi_terminal_styling{$this_style}; - $before = "$before\e[${code}m"; - } - } - if ($before ne '') {$after="\e[0m"} - return "$before$x$after"; -} - -sub length { - my $x = shift; - if (!($x =~ m/\e/)) {return length $x} - $x =~ s/\e\[\d+m//g; - return length $x; -} - -sub pad_to_desired_length { - my $x = shift; - my $desired_length = shift; - my $pad_with = shift; - my $current_length = AnsiTerminalStyling::length($x); - if ($current_length>=$desired_length) {return $x} - return $x . ($pad_with x ($desired_length-$current_length)); -} - -#---------------------------------------------------------------- -# Unicode helper routines: -#---------------------------------------------------------------- - -package UnicodeTools; - -# Note that this may turn a single unicode character into two characters, e.g., with Danish o_slash going to 'oe'. -# It doesn't just do what it says, it basically transliterates everything into ascii, e.g., Greek -# lambda becomes l. -# This gets tested by "when --test_accent_filtering", which is done by "make test". -# Doesn't do anything to Cyrillic. -sub filter_out_accents { - my $x = shift; - - - # First do everything that translates into a single character: - - my @t = ( - ["\x{c2}",'A'],["\x{c4}",'A'],["\x{ce}",'I'],["\x{d6}",'O'],["\x{da}",'U'], - ["\x{dc}",'U'],["\x{df}",'s'],["\x{e0}",'a'],["\x{e1}",'a'],["\x{e2}",'a'], - ["\x{e4}",'a'],["\x{e3}",'a'],["\x{e7}",'c'], - ["\x{e9}",'e'],["\x{ea}",'e'],["\x{ed}",'i'],["\x{ee}",'i'],["\x{f1}",'n'], - ["\x{f3}",'o'],["\x{f5}",'e'],["\x{f6}",'o'],["\x{fa}",'u'],["\x{fb}",'u'],["\x{fc}",'u'], - ["\x{fd}",'y'],["\x{2011}",'-'],["\x{105}",'a'],["\x{103}",'a'],["\x{102}",'A'], - ["\x{107}",'c'],["\x{10d}",'c'],["\x{10c}",'C'],["\x{11b}",'e'],["\x{119}",'e'], - ["\x{142}",'l'],["\x{144}",'n'],["\x{159}",'r'],["\x{158}",'r'],["\x{15b}",'s'], - ["\x{219}",'s'],["\x{218}",'S'],["\x{21b}",'t'],["\x{21a}",'T'],["\x{17a}",'z'], - ["\x{391}",'A'],["\x{3b1}",'a'],["\x{3ac}",'a'],["\x{392}",'B'],["\x{3b2}",'b'], - ["\x{393}",'g'],["\x{3b3}",'g'],["\x{394}",'D'],["\x{3b4}",'d'],["\x{395}",'E'], - ["\x{3b5}",'e'],["\x{3ad}",'e'],["\x{396}",'Z'],["\x{3b6}",'z'],["\x{397}",'H'], - ["\x{3b7}",'n'],["\x{3ae}",'n'],["\x{399}",'I'],["\x{3b9}",'i'],["\x{3af}",'i'], - ["\x{39a}",'K'],["\x{3ba}",'k'],["\x{39b}",'L'],["\x{3bb}",'l'],["\x{39c}",'M'], - ["\x{3bc}",'m'],["\x{39d}",'N'],["\x{3bd}",'v'],["\x{39f}",'O'],["\x{3bf}",'o'], - ["\x{3cc}",'o'],["\x{3a0}",'P'],["\x{3c0}",'p'],["\x{3a1}",'R'],["\x{3c1}",'r'], - ["\x{3a3}",'S'],["\x{3c3}",'s'],["\x{3c2}",'s'],["\x{3a4}",'T'],["\x{3c4}",'t'], - ["\x{3a5}",'Y'],["\x{3c5}",'u'],["\x{3cd}",'u'],["\x{3a6}",'F'],["\x{3c6}",'f'], - ["\x{3a7}",'X'],["\x{3c7}",'x'],["\x{3a9}",'W'],["\x{3c9}",'w'],["\x{3ce}",'w'] - ); - - my $b = ''; - foreach my $c(split('',$x)) { - foreach my $t(@t) { - if ($c eq $t->[0]) {$c=$t->[1]} - } - $b = $b . $c; - } - $x = $b; - - $x =~ s/\x{201c}/''/go; - $x =~ s/\x{ab}/<</go; - $x =~ s/\x{bb}/>>/go; - $x =~ s/\x{c5}/AA/go; - $x =~ s/\x{c6}/AE/go; - $x =~ s/\x{d8}/OE/go; - $x =~ s/\x{e5}/aa/go; - $x =~ s/\x{e6}/ae/go; - $x =~ s/\x{f8}/oe/go; - $x =~ s/\x{201e}/,,/go; - $x =~ s/\x{17c}/zz/go; - $x =~ s/\x{398}/Th/go; - $x =~ s/\x{3b8}/th/go; - $x =~ s/\x{39e}/Ks/go; - $x =~ s/\x{3be}/ks/go; - $x =~ s/\x{3a8}/Ps/go; - $x =~ s/\x{3c8}/ps/go; - return $x; -} - -sub test_accent_filtering { - my @strings = @_; - my $result = ''; - foreach my $x(@strings) { - chomp $x; - my $y = filter_out_accents($x); - while ($y=~/(\P{IsASCII})/g) { - my $c = $1; - if (is_cyrillic($c)) {next} - my $describe_c = sprintf("%04x", ord($c)); - $result = $result . "In the string '$x', the character $1, character code 0x$describe_c, was not properly filtered by UnicodeTools::filter_out_accents().\n"; - } - } - return $result; -} - -sub is_cyrillic { - # tests a single character - my $x = shift; - if ($x=~/[АаБбВвГгДдЕеЁёЖжЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЪъЫыЬьЭэЮюЯяієћњјљї]/) {return 1} else {return 0} - # list from https://codegolf.stackexchange.com/questions/127677/print-the-russian-cyrillic-alphabet , plus a few more in strings people submitted -} - -sub make_filter_regex { - my $filter = filter(); - my @a = sort keys %$filter; - - my $from = ''; - my $to = ''; - my $doubles = ''; - foreach my $a(@a) { - my $b = $filter->{$a}; - my $hex = sprintf('%x',ord($a)); - if ($a eq $b) {print "Warning, $a and $b are the same, in make_filter_regex.\n"} - if (length($b)==1) { - $from = $from . "\\x{$hex}"; - $to = $to . $b; - } - else { - $doubles = $doubles . " \$x =~ s/\\x{$hex}/$b/go;\n"; - } - } - return " \$x =~ tr/$from/$to/;\n # ... everything that translates into a single character\n$doubles"; -} - -BEGIN { - my $filter; - sub filter { - return $filter if $filter; - my %filter = ( - $e_acute=>'e',$A_uml=>'A',$a_uml=>'a',$O_uml=>'O',$o_uml=>'o',$U_uml=>'U',$u_uml=>'u',$s_zlig=>'s',$u_circumflex=>'u', - $a_polish=>'a',$c_polish=>'c',$e_polish=>'e',$l_polish=>'l',$n_polish=>'n',$o_polish=>'o',$s_polish=>'s',$z_polish=>'z',$zz_polish=>'zz', - $a_acute=>'a',$i_acute=>'i',$u_acute=>'u',$U_acute=>'U',$y_acute=>'y',$C_wedge=>'C',$c_wedge=>'c',$e_wedge=>'e',$R_wedge=>'r',$r_wedge=>'r', - $a_ring=>'aa',$A_ring=>'AA',$o_slash=>'oe',$O_slash=>'OE',$ae=>'ae',$AE=>'AE',$n_tilde=>'n', - # Greek. Note that some letters that look like latin really aren't. - 'Α'=>'A','α'=>'a','ά'=>'a','Β'=>'B','β'=>'b','Γ'=>'g','γ'=>'g','Δ'=>'D','δ'=>'d','Ε'=>'E','ε'=>'e','έ'=>'e', - 'Ζ'=>'Z','ζ'=>'z', - 'Η'=>'H','η'=>'n','ή'=>'n','Θ'=>'Th','θ'=>'th', - 'Ι'=>'I','ι'=>'i','ί'=>'i','Κ'=>'K','κ'=>'k','Λ'=>'L','λ'=>'l','Μ'=>'M','μ'=>'m','Ν'=>'N','ν'=>'v', - 'Ξ'=>'Ks','ξ'=>'ks','Ο'=>'O','ο'=>'o','ό'=>'o', - 'Π'=>'P','π'=>'p','Ρ'=>'R','ρ'=>'r','Σ'=>'S','σ'=>'s','ς'=>'s','Τ'=>'T','τ'=>'t','Υ'=>'Y','υ'=>'u','ύ'=>'u', - 'Φ'=>'F','φ'=>'f','Χ'=>'X','χ'=>'x', - 'Ψ'=>'Ps','ψ'=>'ps','Ω'=>'W','ω'=>'w','ώ'=>'w', - # Romanian - $A_breve=>'A',$A_circumflex=>'A',$I_circumflex=>'I',$S_commabelow=>'S',$T_commabelow=>'T', - $a_breve=>'a',$a_circumflex=>'a',$i_circumflex=>'i',$s_commabelow=>'s',$t_commabelow=>'t', - $nonbreaking_hyphen=>'-',$quot_open=>',,',$quot_close=>"''",$quotalt_open=>'<<',$quotalt_close=>'>>' - ); - $filter = \%filter; - return $filter; - } -} - -sub file_is_valid_utf8 { - my $f = shift; - open(F,"<:raw",$f) or return 0; - local $/; - my $x=<F>; - close F; - return is_valid_utf8($x); -} - -# What's passed to this routine has to be a stream of bytes, not a utf8 string in which the characters are complete utf8 characters. -# That's why you typically want to call file_is_valid_utf8 rather than calling this directly. -sub is_valid_utf8 { - my $x = shift; - return utf8::decode(my $dummy = $x); -} - -#----------------------------------------- -# Easter -#----------------------------------------- - -package Easter; - -sub easter { - my $year = shift; - my $sub = $preferences{'orthodox_easter'} ? \&eastern_easter : \&western_easter; - return &$sub($year); -} - -# The following code for Easter is based on Rick Measham's DateTime::Event::Easter module, -# http://search.cpan.org/dist/DateTime-Event-Easter/lib/DateTime/Event/Easter.pm , -# which is available under the same license as Perl itself, and therefore compatible with -# the licensing scheme of When. -# For testing, see: http://en.wikipedia.org/wiki/Easter#Date_of_Easter - -sub western_easter { - my $year = shift; - my $golden_number = $year % 19; - my $quasicentury = int($year / 100); - my $epact = ($quasicentury - int($quasicentury/4) - int(($quasicentury * 8 + 13)/25) + ($golden_number*19) + 15) % 30; - my $interval = $epact - int($epact/28)*(1 - int(29/($epact+1)) * int((21 - $golden_number)/11) ); - my $weekday = ($year + int($year/4) + $interval + 2 - $quasicentury + int($quasicentury/4)) % 7; - my $offset = $interval - $weekday; - my $month = 3 + int(($offset+40)/44); - my $day = $offset + 28 - 31* int($month/4); - return When->new($year,$month,$day); -} - -# The following algorithm for Eastern Orthodox Easter is from George Vlahavas: -# If you put this in a -# spreadsheet cell (gnumeric or openoffice calc will do): -# =MOD(19*MOD(A1;19)+16;30)+MOD(2*MOD(A1;4)+4*MOD(A1;7)+6*MOD(19*MOD(A1;19)+16;30);7)+3 -# and you put the year in cell A1 you will get a number. If this number -# is <=30 then that's the April day that Easter is for that year. If the -# number is >30 then you need to subtract 30 from it and you will find -# the day in May Easter is. - -sub eastern_easter { - my $year = shift; - my $day= - (19*($year%19)+16)%30 - +( - 2*($year%4) - +4*($year%7) - +6*( - ( - 19*($year%19)+16 - )%30 - ) - )%7 - +3; - my $month; - if ($day<=30) {$month=4} else {$month=5; $day=$day-30} - return When->new($year,$month,$day); -} - -#----------------------------------------- -# Terminal -#----------------------------------------- - -package Terminal; - -# Normally returns the number of columns on the output tty. -# Return 0 if output isn't a tty. -# Returns undef if it's unable to find the width. -# Tries several methods, in an attempt to work on any platform, while being as -# efficient as possible in most cases, and avoiding dependencies. This does *not* -# introduce any dependencies on Term::ReadKey or Term::ReadLine unless you're -# using a non-POSIX system; those are used only as last-ditch backup methods in -# case, e.g., we're running Windows. -sub columns { - return (get_data())[1]; -} - -sub rows { - return (get_data())[0]; -} - -BEGIN { -my ($rows,$columns); -my $initialized = 0; - -sub get_data { - initialize_data() unless $initialized; - return ($rows,$columns); -} - -sub initialize_data { - $initialized = 1; - ($rows,$columns) = get_data_for_initialization(); - $SIG{WINCH} = sub{$initialized=0}; # If the window gets resized, we get this signal. -} - -# Return the number of rows and columns on the terminal. -sub get_data_for_initialization { - - return (0,0) unless -t STDOUT; - - my ($r, $c, $dummy); - - # The following works on linux, but seems to fail on freebsd. - # It works properly if the user resizes the terminal window while the program is running. - # No longer works in perl 5.10, so disabled with "0 &&". - # http://search.cpan.org/src/RGARCIA/perl-5.10.0/h2pl/README - if (0 && eval "require 'sys/ioctl.ph'") { - eval { - # All of the dies on the next few lines will be caught by the eval{}. - die unless defined &TIOCGWINSZ; - open(TTY, "+</dev/tty") or die; - my $winsize; - die unless ioctl(TTY, &TIOCGWINSZ, $winsize=''); - ($r, $c, $dummy, $dummy) = unpack('S4', $winsize); - return ($r,$c); - } - } - - # A less efficient fallback, should work on anything unixy. - chomp(my @lines = `stty -a`); - for (@lines) { - $r = $1 if /rows (\d+);/; # linux - $r = $1 if /(\d+) rows;/; # FreeBSD - $c = $1 if /columns (\d+);/; # linux - $c = $1 if /(\d+) columns;/; # FreeBSD - } - return ($r,$c) if ((defined $c) && (defined $r)); - - # The following two methods give us a fighting chance on a non-POSIX system. I don't use them as the defaults - # because I don't want to introduce dependencies. - - # http://search.cpan.org/~kjalb/TermReadKey/ReadKey.pm - # Term::ReadKey is not a standard Perl module, and may not be installed. If the user resizes the terminal - # while the program is running, this will correctly reflect the resizing. - eval 'use Term::ReadKey; ($c, $r, $dummy, $dummy) = GetTerminalSize();'; - return ($r,$c) if ((defined $c) && (defined $r)); - - # http://search.cpan.org/~nwclark/perl-5.8.8/lib/Term/ReadLine.pm - # Term::ReadLine is a standard Perl module, but exists in different implementations under the hood. On a Linux - # system, it's implemented using Term::ReadLine::Gnu, which supports get_screen_size(). On a default FreeBSD system, - # however, the following won't work; you'd have to install the p5-ReadLine-Gnu package to get support for this function. - eval 'use Term::ReadLine; $term = new Term::ReadLine("foo"); ($r,$c)= Term::ReadLine::get_screen_size();'; - return ($r,$c) if ((defined $c) && (defined $r)); - - return undef; -} -} |
