#!/usr/bin/perl 

#----------------------------------------------------------------
#   When
#   a simple personal calendar
#   (c) 2003-2005 Benjamin Crowell
#   This free software is copyleft licensed under the same terms as Perl, or,
#   at your option, under version 2 of the GPL license.
#----------------------------------------------------------------

#----------------------------------------------------------------
# Version number
#   This is the one place where we set the version number. It's
#   read by the Makefile and by web_page_template.pl. They do
#   a `when --bare_version' to get this spit out.
#----------------------------------------------------------------
our $version = "1.1.0"; 

#----------------------------------------------------------------
# Short documentation
#
# This is what is printed out if you do `when --help'. The more
# detailed documentation is after this in the source code, and
# the man page is generated from that.
#----------------------------------------------------------------

sub documentation {
return <<'DOC';
   When
   a simple personal calendar
   (c) 2003-2006 Benjamin Crowell
   This free software is copyleft licensed under the same terms as Perl, or,
   at your option, under version 2 of the GPL license.

   The full documentation for When is in its man page, which you can access
   (after installing the software) by doing the command `man when'. If you want
   to read the documentation without first installing the software, use the
   command `nroff -man when.1', or go to http://www.lightandmatter.com/when/when.html
   and look at the reproduction of the man page there.

   To install the program, do the command `make install' while logged in as
   the root user.

   The basic idea is just to type `when' at the command line. The first time
   you run the program, it will prompt you for some setup information. To
   edit you calendar file in your favorite editor, do `when e'. The basic
   format of the calendar file is like this:
        2003 feb 3 , Fly to Stockholm to accept Nobel Prize.
   Once you have a calendar file, running the program as plain old `when'
   from the command line will print out the things on your calendar for the
   next two weeks.

DOC
}

#----------------------------------------------------------------
# Long documentation
#
# The man page is generated from this, using pod2man.
#----------------------------------------------------------------

=head1 NAME

When - a minimalistic personal calendar program

=head1 SYNOPSIS

when

when [options] [commands]

=head1 COMMANDS

=over 8

=item B<i>

Print upcoming items on your calendar. (This is the default command.)

=item B<c>

Print calendars for last month, this month, and next month.

=item B<e>

Invoke your favorite editor to edit your calendar file.

=item B<w>,B<m>,B<y>

Print items for the coming week, month, or year, rather
than for the default period of two weeks.

=item B<j>

Print the modified Julian day (useful for finding the time interval
between two dates).

=item B<d>

Print nothing but the current date.

=back

=head1 OPTIONS

All of the following options, except --help, can be set in the preferences
file. True/false options can be set on the command line as --option or
--nooption, and in the preferences file by setting option to 0 or 1.

=over 8

=item --help

Prints a brief help message.

=item --version

Prints a brief message, including a statement of what version of the
software it is.

=item --language=LANG

Set the language to LANG. See the section below on internationalization.
This option is not normally needed, because the language is automatically
detected.

=item --past=DAYS

How many days into the past the report extends. Default: -1

=item --future=DAYS

How many days into the past the report extends. Default: 14

=item --calendar=FILE

Your calendar file. The default is to use the file pointed to
by your preferences file, which is set up the first time you
run When.

=item --wrap=COLUMNS

Number of columns of text for the output (or 0 if you don't want
wrapping at all). Default: 80

=item --rows=COLUMNS

Number of rows of text that will fit in the terminal window.
When listing your calendar, output will be truncated to this
length, unless that would result in listing less than three days
into the future. This behavior is overridden (the maximum number
of rows is set to infinity) if the --future option is given
explicitly on the command line, or if the m or y command is
used.
Default: 30

=item --editor=COMMAND

Command used to invoke your editor. Default: emacs -nw
Example:  when --editor="vi -R"

=item --[no]filter_accents_on_output

Whether to change accented characters to unaccented ones.
Default: yes, unless the $TERM environment variable equals "mlterm"
or "xterm".

=item --[no]styled_output

If the output is a terminal, should we use ANSI terminal codes for
styling? Default: yes

=item --[no]styled_output_if_not_tty

Style the output even if it's not a terminal. Default: no

=item --calendar_today_style=STYLE

How to style today's date when doing the calendar (c) command.
Default: bold

=item --items_today_style=STYLE

How to style the word ``today'' when doing the items (i) command.
Default: bold

=item --now="Y M D"

Pretend today is some other date.

=item --[no]monday_first

Start the week from Monday, rather than Sunday. Default: no

=item --[no]ampm

Display the time of day using 12-hour time, rather than 24-hour time.
Default: yes

=item --test_expression

Used internally by `make test'.

=item --bare_version

Used internally by the Makefile.

=back

The styling of output can be specified using the following keywords:
bold, underlined, flashing.
To change the color of the text, use these:
fgblack, fgred, fggreen, fgyellow, fgblue, fgpurple, fgcyan,
fgwhite.
To change the background color, use similar keywords, but with bg instead
of fg. Example:
when --calendar_today_style="bold,fgred,bgcyan" c

=head1 DESCRIPTION

B<When> is an extremely simple personal calendar program, aimed at the Unix
geek who wants something minimalistic. It can keep track of things you need to
do on particular dates. There are a lot of calendar and ``personal information
manager'' programs out there, so what reasons are there to use When?

=over 4

=item It's a very short and simple program, so you can easily tinker with it
  yourself.

=item It doesn't depend on any libraries, so it's easy to install. You should
  be able to install it on any system where Perl is available, even if you
  don't have privileges for installing libraries.

=item Its file format is a simple text file, which you can edit in your favorite
  editor.

=back

Although When should run on virtually any operating system where Perl is
available, in this document I'll assume you're running some
flavor of Unix.

=head1 INSTALLATION AND GETTING STARTED

While logged in as root, execute the following command:

       make install

Run When for the first time using this command:

       when

You'll be prompted for some information needed to set up your calendar file.

=head1 USE

If you run When again after the initial setup run, it should print out a
single line of text, telling you the current date. It won't print out
anything else, because your calendar file is empty, so you don't have
any appointments coming up.

Now you can start putting items in your calendar file. Each item is a line
of text that looks like this:

        2003 feb 3 , Fly to Stockholm to accept Nobel Prize.

A convenient way to edit your calendar file is with this command:

        when e

This pops you into your favorite editor (the one you chose when you ran
When for the first time).

The date has to be in year-month-day format, but you can either spell the
month or give it as a number. (Month names are case-insensitive, and it
doesn't matter if you represent February as F, Fe, Feb, Februa, or whatever.
It just has to be a unique match. You can give a trailing ., which will be
ignored.) Extra whitespace is
ignored until you get into the actual text after the comma. Blank lines
and lines beginning with a # sign are ignored.

For events that occur once a year, such as birthdays and annivesaries,
you can either use a * in place of the year,

        * dec 25 , Christmas

or use a year with an asterisk:

        1920* aug 29 , Charlie Parker turns \a, born in \y

In the second example, \a tells you how old Charlie Parker would be this
year, and \y reproduces the year he was born, i.e., the output would be:

        today     2003 Aug 29 Charlie Parker turns 83, born in 1920

For things you have to do every week, you can use an expression of the
form w=xxx, where xxx is the first few letters of the name of the day
of the week in your language. (You have to supply enough letters to
eliminate ambiguity, e.g., in English, w=th or w=tu, not just w=t.)
Example:

        w=sun , go to church, 10:00

You can actually do fancier tests than this as well; for more information,
see the section 'fancy tests' below.

If you now run When, it will print out a list of all the items in your
calendar file that fall in the time interval from yesterday through
the next two weeks. To see all your items for the next month, do ``when m'',
and similarly for a year, y, or a single week, w.

If you do ``when c'', When prints out calendars for last month, this month,
and next month.

You can combine these commands. For instance, ``when cw'' will print
out calendars, and then show you your items for the next week.

=head1 INTERNATIONALIZATION

When has at least partial support for English, Polish, Italian,
German, Dutch, and French.
If When has not been translated into your language, or has only
been partially translated, the text that hasn't been translated
will be displayed in English.
When should automatically detect what language you use (via your $LANG
environment variable), and if When has been translated into that language,
that's what you'll get -- When's output will be in your language, and
When will also expect you to use that language in your calendar file
for the names of the months and the days of the week.

Some terminal emulators
(aterm, ...) display accented characters as garbage,
but others (mlterm, xterm...) can display them correctly.
When checks the $TERM environment variable, and if it equals
"mlterm" or "xterm", then accented characters will be displayed. Otherwise,
they are filtered out of the output.
You can override this by putting a line like

        filter_accents_on_output = 0

or

        filter_accents_on_output = 1

in your ~/.when/preferences file. I'd be interested in hearing from
any users who can suggest a better mechanism for this than attempting to
interpret the $TERM variable.

On input, accents are allowed, but not required, e.g., in a French-language
input file, the date 2005 Fev 17 could be given with an accented e or an
unaccented one, and either will work. The input file and
command-line options can be encoded in UTF-8, or plain ASCII (which
is a subset of UTF-8). If an input month or day of the week does
not match any of the ones for your language, then When will try
to interpret it as English instead.

You can put a line like

        language = fr

in your preferences file to set your language, or supply the --language
option on the command line, but that's not necessary if your $LANG
environment variable is set correctly.

=head1 FORMAT OF THE PREFERENCES FILE

Each line consists of something like this:

        variable = value

Whitespace is ignored everywhere except inside the value. Variable names
are case-insensitive. Blank lines are ignored.

=head1 MORE EXAMPLES

A useful command to have your shell execute when you log in is this:

        when --past=0 --future=1

To print out a calendar for a full year to come:

        when --past=0 --future=365 c

=head1 POPPING UP YOUR CALENDAR WHEN YOU LOG IN

Your calendar doesn't do you any good if you forget to look at it
every day. An easy way to make it pop up when you log in is to
make your .xsession or .xinitrc file look like this:

        /usr/bin/when --past=0 --future=1 &>~/when.today
        emacs -geometry 70x25 -bg bisque ~/when.today &
        startkde

The .xsession file is used if you have a graphical login manager
set up on your machine, the .xinitrc if you don't. In this example,
the first line outputs your calendar to a file. The complete path
to the When program is given, because your shell's path variable
will not yet be properly initialized when this runs. The second
line pops up a GUI emacs window, which is distinctively colored so
that it will catch your eye. The last line starts your window
manager, KDE in this example. Whatever window manager you use,
just make sure to retain the preexisting line in the file that starts
it, and make sure that that line is the very last one in the file.

=head1 FANCY TESTS

In addition to w, discussed above, there are a bunch of other variables
you can test:

	w  -  day of the week
	m  -  month
	d  -  day of the month
	y  -  year
	j  -  modified Julian day number
	a  -  1 for the first 7 days of the month, 2 for the next 7, etc.
	b  -  1 for the last 7 days of the month, 2 for the previous 7, etc.

You can specify months either as numbers, m=2, or as names in your language, m=feb.
You can also use the logical operators & (and) and | (or). The following 
example reminds you to pay your employees on the first and fifteenth
day of every month:

        d=1 | d=15 , Pay employees.

This example reminds you to rehearse with your band on the last Saturday
of every month:

        w=sat & b=1 , Rehearse with band.

The following two lines

        * dec 25 , Christmas
        m=dec & d=25 , Christmas

both do exactly the same thing, but the first version is easier to
understand and makes the program run faster. (When you do a test, When
has to run through every day in the range of dates you asked for,
and evaluate the test for each of those days. On my machine, if I
print out a calendar for a whole year, using a file with 10 simple
tests in it, it takes a few seconds.) Parentheses can be used, too.
There is a not operator, !:

        w=fri & !(m=dec & d=25) , poker game

There is a modulo operator, %, and a subtraction operator, -. 
Using these, along with the j variable, it is just barely possible
for When's little parser to perform the following feat:

        !(j%14-1) , do something every other Wednesday

The logic behind this silly little piece of wizardry goes like this.
First, we determine, using the command `when j --now="2005 jan 26"',
that the first Wednesday on which we want to
do this has a Julian day that equals 1, modulo 14. Then we write this
expression so that if it's a Wednesday whose Julian day equals 1,
modulo 14, the quantity in parentheses will be zero, and taking its logical
negation will yield a true value.

The operators' associativity and order of priority (from highest to lowest) is
like this:

	left	%
	left	-
	left	< > <= >=
	left	= !=
	right	!
        left	&
	left	|


=head1 ENVIRONMENT

B<$LANG> to automatically detect the user's language

B<$TERM> to try to figure out if the terminal emulator can display
accented characters

=head1 FILES

B<$HOME>/.when/calendar - The default location for the
user's calendar (pointed to by the preferences file)

B<$HOME>/.when/preferences - The user's preferences.

=head1 OTHER INFORMATION

When's web page is at

        http://www.lightandmatter.com/when/when.html   ,

where you can always find the latest version of the software.
There is a page for When on Freshmeat, at

        http://freshmeat.net/projects/when/   ,

where you can give comments, rate it, and subscribe to e-mail announcements of new releases.

=head1 AUTHOR

When was written by Ben Crowell, http://www.lightandmatter.com/personal/.
Dimiter Trendafilov wrote the new and improved parser for date expressions.
This man page was based on Bryce Harrington's man page for inkscape.

=head1 COPYRIGHT AND LICENSE

B<Copyright (C)> 2003-2005 by Benjamin Crowell.

B<When> is free software; you can redistribute it and/or modify it
under the terms of the GPL, or, optionally, Perl's license.

=cut

#================================================================
#
# beginning of real code
#
#================================================================

use strict;
use locale;
use utf8;
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
  'rows'=>40,                       # try to limit output to less than this number of lines
  '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
  '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.
  'monday_first'=>0,                # display Monday rather than Sunday as the first day of the week?
  'ampm'=>1,                        # use 12-hour time?
  'text_expression'=>''             # used by 'make test'
);

our %options=(
  'help'=>0,                        # print documentation
  'version'=>0,                     # print version number, and other info
  'bare_version'=>0,                 # print version number
);

our %command_line_options = (
  'help'=>\$options{'help'},
  'version'=>\$options{'version'},
  'bare_version'=>\$options{'bare_version'},
  'language=s'=>\$preferences{'language'},
  'past=i'=>\$preferences{'past'},
  'future=i'=>\$preferences{'future'},
  'calendar=s'=>\$preferences{'calendar'},
  'wrap=i'=>\$preferences{'wrap'},
  'editor=s'=>\$preferences{'editor'},
  'now=s'=>\$preferences{'now'},
  'calendar_today_style=s'=>\$preferences{'calendar_today_style'},
  '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'},
  'ampm!'=>\$preferences{'ampm'},
  'test_expression=s'=>\$preferences{'test_expression'},
);

#----------------------------------------------------------------
# Strings are all collected here for ease of internationalization:
#----------------------------------------------------------------

# When adding characters to the following list, make sure to add them to
# filter_out_accents() as well.
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}";


#**********************************************************************
# 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
   'pl'=>"Sty Lut Mar Kwi Maj Cze Lip Sie Wrz Pa${z_polish} Lis Gru", # Polish
   'fr'=>"Jan F${e_acute}v Mar Avr Mai Juin Juil Ao${u_circumflex} Sep Oct Nov D${e_acute}c", # French
   'it'=>"Gen Feb Mar Apr Mag Giu Lug Ago Set Ott Nov Dic", # Italian (by Mario)
   'de'=>'Jan Feb Mar Apr Mai Jun Jul Aug Sep Okt Nov Dez', # German
   'nl'=>'Jan Feb Mar Apr Mei Jun Jul Aug Sep Okt Nov Dec', # Dutch
  );
our %month_name_long =
  (
    # English:
    'en'=>'January February March April May June July August September October November December',
    # Polish:
    'pl'=>"Stycze${n_polish} Luty Marzec Kwiecie${n_polish} Maj Czerwiec Lipiec Sierpie${n_polish} Wrzesie${n_polish} Pa${z_polish}dziernik Listopad Grudzie${n_polish}",
    # French:
    'fr'=>"Janvier F${e_acute}vrier Mars Avril Mai Jiun Juillet Ao${u_circumflex}t Septembre Octobre Novembre D${e_acute}cembre",
    # Italian:
    'it'=>'Gennaio Febbraio Marzo Aprile Maggio Giugno Luglio Agosto Settembre Ottobre Novembre Dicembre', 
    # German:
    'de'=>"Januar Februar M${a_uml}rz April Mai Juni Juli August September Oktober November Dezember",
    # Dutch:
    'nl'=>'Januari Februari Maart April Mei Juni Juli Augustus September Oktober November December',
  );
our %wday_name = 
  (
    'en'=>'Sun Mon Tue Wed Thu Fri Sat', # English
    'pl'=>'Pon Wto Sro Czw Pia Sob Nie', # Polish
    'it'=>'Dom Lun Mar Mer Gio Ven Sab', # Italian
    'de'=>'So Mo Di Mi Do Fr Sa',        # German
    'nl'=>'zondag maandag dinsdag woensdag donderdag vrijdag zaterdag', # Dutch (not capitalized)
  );

#------------------------------------------------------------------------
# This subroutine is used every time we need to print out some text in the
# user's chosen language.
#------------------------------------------------------------------------
sub w {
  my $what = shift;
  my @stuff = @_;
  my $lingo = $preferences{'language'};
  my %strings;
  if ($lingo eq 'it' ) { # Italian (thanks to Mario)
    %strings = 
      (
        'error_opening_prefs'=>"Il file delle preferenze %s esiste, ma ci sono problemi durante la lettura.",
        'syntax_err_in_prefs'=>"Errore di sintassi nelle preferenze %s:\n%s",
        'prefs_file_not_found'=>"File delle preferenze %s non trovato.\n",
        'illegal_command'=>"Comando illegale, %s\n",
        'error_opening_calendar'=>"Non posso aprire il file %s per la lettura\n",
        'yesterday'=>'ieri',
        'today'=>'oggi',
        'tomorrow'=>'domani',
        'syntax_error_in_calendar'=>"Errore di sintassi nel calendario: %s\n",
        'illegal_year'=>"Anno illegale: %s\n",
        'illegal_month'=>"Mese illegale: %s\n",
        'illegal_day_of_month'=>"Giorno del mese illegale: %s\n",
        'first_time_not_tty'=>"Per impostare il tuo calendario, richiama il comando ``when'' in un terminale interattivo.\n",
        'ask_if_set_up'=>("Adesso puoi impostare il tuo calendario. Questo avviene creando una directory ~/.when e creando\n".
                          "un paio di file al suo interno. Se si desidera procedere, digitare y e premere invio.\n"),
        'error_creating_dir'=>"Errore durante la creazione della directory %s\n",
        'ask_for_editor'=>("Puoi modificare il tuo calendario usando il tuo editor preferito. Per favore, inserire il comando\n"
                           ."che si vuole usare per lanciare il tuo editor, o premere invio per accettare quello predefinito:\n"),
        'error_creating_prefs'=>"Errore durante la creazione del file %s\n",
        'error_creating_cal'=>"Errore durante la creazione del file %s\n",
        'getting_started'=>("Adesso puoi aggiungere degli elementi al tuo calendario. Usare ``when --help'' per maggiori informazioni.\n"),
      )
    } elsif ($lingo eq 'pl' ) { # Polish (thanks to Marcin Omen)
        %strings = 
      (
        'date_syntax_error'=>"Podana data %s jest w niew${l_polish}a${s_polish}ciwym formacie. Podaj dat${e_polish} w formacie 'y m d', ze spacjami oddzielaj${a_polish}cymi poszczeg${o_polish}lne cz${e_polish}${s_polish}ci.",
        'error_opening_prefs'=>"Plik konfiguracyjny %s istnieje, jednak wyst${a_polish}pi${l_polish} b${l_polish}${a_polish}d podczas jego otwierania.",
        'syntax_err_in_prefs'=>"B${l_polish}${a_polish}d sk${l_polish}adni w pliku konfiguracyjnym %s:\n%s",
        'prefs_file_not_found'=>"Plik konfiguracyjny %s nie zosta${l_polish} odnaleziony.\n",
        'illegal_command'=>"Niew${l_polish}a${s_polish}ciwa komenda %s\n",
        'error_opening_calendar'=>"B${l_polish}${a_polish}d przy otwieraniu pliku %s \n",
        'yesterday'=>'wczoraj',
        'today'=>("dzi${s_polish}"),
        'tomorrow'=>'jutro',
        'syntax_error_in_calendar'=>"B${l_polish}${a_polish}d sk${l_polish}adni w pliku kalendarza: %s\n",
        'illegal_year'=>"Niew${l_polish}a${s_polish}ciwy rok: %s\n",
        'illegal_month'=>"Niew${l_polish}a${s_polish}ciwy miesi${a_polish}c: %s\n",
        'illegal_day_of_month'=>"Niew${l_polish}a${s_polish}ciwy dzie${n_polish}: %s\n",
        'first_time_not_tty'=>"Aby zainicjowac kalendarz u${zz_polish}yj komendy ``when'' w oknie terminala.\n",
        'ask_if_set_up'=>("Za chwil${e_polish} rozpocznie sie konfiguracja kalendarza. Stworzony zostanie katalog ~/.when w\n kt${o_polish}rym pojawi${a_polish} sie pliki konfiguracyjne. Je${s_polish}li zgadzasz si${e_polish} na t${a_polish} operacj${e_polish} wci${s_polish}nij\n klawisz 'y' i potwierd${z_polish} klawiszem Enter.\n"),
        'error_creating_dir'=>"B${l_polish}${a_polish}d w tworzeniu katalogu %s\n",
        'ask_for_editor'=>("Mo${zz_polish}esz edytowa${c_polish} sw${o_polish}j plik kalendarza u${zz_polish}ywaj${a_polish}c ulubionego edytora. Podaj komend${e_polish} do uruchamienia\n".
                	"edytora lub wci${s_polish}nij Enter aby u${zz_polish}ywa${c_polish} standardowego edytora:\n"),
        'error_creating_prefs'=>"B${l_polish}${a_polish}d w tworzeniu pliku %s\n",
        'error_creating_cal'=>"B${l_polish}${a_polish}d w tworzeniu pliku %s\n",
        'getting_started'=>("Mo${zz_polish}esz teraz dodawa${c_polish} nowe pozycje do kalendarza. Sprawd${z_polish} ``when --help'' by otrzymać więcej informacji.\n"),
        'not_unique_w_match'=>'%s nie jest zgodny z unikalnym dniem tygodnia z %s',
        'no_w_match'=>'%s nie jest zgodny z dniem tygodnia z %s',
        'not_valid_expression'=>("%s nie jest prawid${l_polish}ow${a_polish} dat${a_polish} lub wyra${zz_polish}eniem"),
        'illegal_var'=>("niew${l_polish}a${s_polish}ciwa zmienna: %s"),
        'illegal_month_in_expression'=>("b${l_polish}${e_polish}dny miesi${a_polish}c w wyra${zz_polish}eniu: %s"),
        'expression_syntax_error'=>("bl${a_polish}d sk${l_polish}adni: %s"),
      )
    } elsif ($lingo eq 'nl' ) { # Dutch
        %strings = 
      (
        'date_syntax_error'=>"De datum %s is in een onjuist formaat.  Gebruik 'y m d', met spaties tussen de drie delen.",
        'error_opening_prefs'=>"De voorkeuren bestand %s bestaat, maar kan niet openen om te lezen .", 
        'syntax_err_in_prefs'=>"Syntaxische fout in voorkeuren bestand %s:\n%s",
        'prefs_file_not_found'=>"Voorkeuren bestand %s niet gevonden.\n",
        'illegal_command'=>"Foute opdracht, %s\n",
        'error_opening_calendar'=>"Kon niet de bestand %s voor invoer openen\n",
        'yesterday'=>'gister',
        'today'=>'vandaag',
        'tomorrow'=>'morgen',
        'syntax_error_in_calendar'=>"Syntaxis fout in kalendar: %s\n",
        'illegal_year'=>"Onjuiste jaar: %s\n",
        'illegal_month'=>"Onjuiste maand: %s\n",
        'illegal_day_of_month'=>"Onjuiste dag van de maand: %s\n",
        'first_time_not_tty'=>"om uw kalendar klaar te maken, doe de opdracht ``when'' aan de commandoregel.\n",
        'ask_if_set_up'=>("u kan nu uj kalendar instellen. Map ~/.when aanmaken daarmee\n".
                          "een paar bestanden daarin. als u wil graag dit te doen, toets y en return.\n"),
        'error_creating_dir'=>"Kon niet de map aanmaken %s\n",
        'ask_for_editor'=>("U kan uw kalendar met uw favoriete editor bewerk. Vul de opdracht u\n"
                           ."wil uw editor te werken, of toets enter op om dit standaard te accepteren:\n"),
        'error_creating_prefs'=>"Kon niet de voorkeuren bestand aanmaken %s\n",
        'error_creating_cal'=>"Kon niet de kalendar bestand aanmaken %s\n",
        'getting_started'=>("Nu kan u zaken naar uw kalendar toevoegen. Toets ``when --help'' voor meer informatie.\n"),
        'not_unique_w_match'=>'%s voldoet niet aan een enig dag van %s',
        'not_valid_expression'=>'%s is een unjuist datum of bewoording',
        'illegal_var'=>'Onjuist variabel: %s',
        'illegal_month_in_expression'=>'Onjuist maand in bewoording: %s',
        'expression_syntax_error'=>'Syntaxische fout: %s',
      )
    } elsif ($lingo eq 'de' ) { # German (thanks to Chis Mager)
        %strings = 
      (
        'date_syntax_error'=>("Das Datum %s ist nicht im ben${o_uml}tigten Format ('y m d'), die drei Teile sind durch Leerzeichen getrennt."),
        'error_opening_prefs'=>("Die Preferences-Datei %s existiert zwar, aber es trat ein Fehler beim ${O_uml}ffnen auf."),
        'syntax_err_in_prefs'=>("Syntaxfehler in der Preferences-Datei %s:\n%s"),
        'prefs_file_not_found'=>("Preferences-Datei %s nicht gefunden.\n"),
        'illegal_command'=>("Ung${u_uml}ltige Anweisung: %s\n"),
        'error_opening_calendar'=>("Konnte die Datei %s nicht ${o_uml}ffnen.\n"),
        'yesterday'=>'Gestern',
        'today'=>'Heute',
        'tomorrow'=>'Morgen',
        'syntax_error_in_calendar'=>("Syntaxfehler in Kalender: %s\n"),
        'illegal_year'=>("Unzul${a_uml}ssiges Jahr: %s\n"),
        'illegal_month'=>("Unzul${a_uml}ssiger Monat: %s\n"),
        'illegal_day_of_month'=>("Unzul${a_uml}ssiger Tag des Monats: %s\n"),
        'first_time_not_tty'=>("Um Ihren Kalernder aufzubauen, f${u_uml}hren Sie den Befehl ``when'' in einem interaktiven Terminalfenster aus.\n"),
        'ask_if_set_up'=>("Sie k${o_uml}nnen jetzt Ihren Kalender aufbauen. Dies hat zur Folge, dass ein Verzeichnis ~/.when angelegt wird, und in ihm\n".
                          "ein paar Dateien. Wenn Sie das machen wollen, dr${u_uml}cken Sie y, gefolgt von ENTER.\n"),
        'error_creating_dir'=>("Fehler beim Anlegen des Verzeichnisses %s\n"),
        'ask_for_editor'=>("Sie k${o_uml}nnen den Kalender mit Ihren Lieblings-Editor bearbeiten. Bitte geben Sie dazu den Befehl zum Starten des Editors ein\n"
                           ."oder dr${u_uml}cken Sie ENTER, um den Standardwert zu akzeptieren:\n"),
        'error_creating_prefs'=>("Fehler beim Anlegen der Kalender Datei %s\n"),
        'error_creating_cal'=>("Fehler beim Anlegen der Preferences-Datei %s\n"),
        'getting_started'=>("Sie k${o_uml}nnen Ihrem Kalender jetzt Termine hinzuf${u_uml}gen. F${u_uml}hren Sie ``when --help'' f${u_uml}r weitere Informationen aus.\n"),
        'not_unique_w_match'=>("Es konnte kein eindeutiger Tag f${u_uml}r %s bestimmt werden. M${o_uml}gliche Tage: %s"),
        'no_w_match'=>("%s stimmt mit keinem der Folgenden Tage ${u_uml}berein: %s"),
        'not_valid_expression'=>("%s ist kein g${u_uml}ltiges Datum oder Ausdruck."),
        'illegal_var'=>("Ung${u_uml}ltige Variable: %s"),
        'illegal_month_in_expression'=>("Ung${u_uml}ltiger Monat im Ausdruck: %s"),
        'expression_syntax_error'=>("Verschachtelte Klammern oder anderer Syntaxfehler: %s"),
      )
    } else { # default to English
         %strings = 
      (
        'date_syntax_error'=>"The date %s is not in the required format, which is 'y m d', with blanks separating the three parts.",
        '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",
        '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, 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'=>'nested parentheses or other syntax error: %s',
      )
    }
  if (exists $strings{$what}) {$what = $strings{$what}}
  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.
#----------------------------------------------------------------
my $dir = glob "~/.when";
my $prefs_file = glob '~/.when/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{'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)) {
  run_first_time($preferences{'calendar'});
}
if (-e $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/\s+$//; # strip trailing blanks
      $preferences{lc($option)} = $value;
    }
    else {
      if (!($line =~ m/^\s*$/)) {die w('syntax_err_in_prefs',$prefs_file,$line)}
    }
  }
  close FILE;
}
else {
  do_output(w('prefs_file_not_found',$prefs_file)) if !$quickie;
}

#----------------------------------------------------------------
# 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 ($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 = 'di'}

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) {
  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 w('date_syntax_error',$preferences{'now'});
  }
  $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};
    $err = w(@$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-2005 Benjamin Crowell.\nDo 'when --help' for help and copyleft information.\n";
  exit(0);
}
if ($options{'bare_version'}) {
  print $version;
  exit(0);
}

my $first_one = 1;
foreach my $cmd(@do_what) {
  if (!$first_one || ($first_one && $preferences{'header'})) {
    do_output("\n");
  }
  $first_one = 0;
  if ($cmd eq '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;
    do_output("$describe_wday $describe_today   $describe_time\n");
  }
  if ($cmd eq 'normal') {
    normal_behavior($now,$want_styling);
  }
  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') {
    system($preferences{'editor'}." ".$preferences{'calendar'});
  }
  if ($cmd eq 'modified_julian_day') {
    print "The date ".$now->string_human()." corresponds to modified julian day ".$now->modified_julian_day().".\n";
  }
}

sub do_output {
  my $x = shift;
  if ($preferences{'filter_accents_on_output'}) {
    $x = UnicodeTools::filter_out_accents($x);
  }
  print $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;

  open (FILE, "<$file") or die w('error_opening_calendar',$file);
  my @lines = <FILE>; # in array context, returns all lines from file
  close FILE;

  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 = AnsiTerminalStyling::pad_to_desired_length($describe,9,' ');
    my $say = sprintf "%s %s %s\n",$describe,$when->string_human,$what;
    push @show,[$when->clone,$say];
  };
  foreach my $line(@lines) {
    chomp $line;
    if ($line =~ m/^\s*([^,#]*)\s*,\s*(([^\s].*)?)/) {
      my ($date,$what) = ($1,$3);
      my $match;
      if ($date=~m/[=<>%]/) {
        $match = DateMatch->new('condition',$date);
        if ($match->{ERR}) {
          my $err = $match->{ERR};
          $err = w(@$err);
          do_output("****** $err\n******  $line\n\n");
          return;
        }
      }
      else {
        $match = DateMatch->new('exact',$date);
        if ($match->{ERR} ne '') {
          my $err = $match->{ERR};
          do_output("****** $err\n******  $line\n\n");
          return;
        }
      }
      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);
        }
      }
    }
    else {
      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


  @show = sort {$a->[0]->compare($b->[0])} @show;

  my $wrap = $preferences{'wrap'};
  if ($wrap==0) {$wrap=9999}
	my $rows = $preferences{'rows'};
  if ($rows==0) {$rows=9999}
  my $margin = 22; # This is the width of the column containing the date.
  my $lines_done = 2; # header and blank line under it
  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;
    do_output($output);
  }
}

#----------------------------------------------------------------
# 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 @cal1 = make_one_month_calendar(WHEN=>$last_month,WANT_STYLING=>$args{WANT_STYLING});
      my @cal2 = make_one_month_calendar(WHEN=>$middle_month,MARK_TODAY=>$row==0,WANT_STYLING=>$args{WANT_STYLING},TODAY_STYLE=>$args{TODAY_STYLE});
      my @cal3 = make_one_month_calendar(WHEN=>$next_month,WANT_STYLING=>$args{WANT_STYLING});

      my @cals = (\@cal1,\@cal2,\@cal3);

      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;
          }
          do_output("$full_line\n");
        }
        else {
          last
        }
      } # end loop over $line_num

          } # end if $needed

  } # end loop over $row

}

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;

  my $line = '';
  for (my $wday=1; $wday<=7; $wday++) {
    $line = $line . ' '.When::short_wday_name($wday).' ';
  }
  push @cal,$line;

  $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) {
    if ($when->day_of_week==1) {push @cal,$line; $line = ''}
    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();
  }
  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";
  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.
sub split_a_line {
  my $text = shift;
  my $width = shift;
  my $max_pieces = shift;
  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 they have 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.
  $text =~ m/^(.{0,$width})(.*)$/;
  my ($a,$b) = ($1,$3);
  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;

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);
    $self->{ERR} = $expr->[0];
    $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/^[abdjmyw]$/) {
      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}
      }
      push @stack,$vars->{$rpn};
    }
    #---- 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;
}

sub compile_expression { # a test such as 'm=dec & d=25'
  my $source = lc shift;
  my @ex = split / */, $source;
  my @rpn;
  my @opst = ('(');	# bottom for the stack
  my ($op, $op2, $pr, $err);

  my $i = 0;
  while ($i < @ex) {
    if ($ex[$i] =~ /\d/) {
      my $num = 0;
      while ($i < @ex && $ex[$i] =~ /\d/) {
	$num = 10 * $num + $ex[$i];
	$i++;
      }
      push @rpn, $num;
    }
    elsif ($ex[$i] =~ /[[:alpha:]]/ && $ex[$i+1] !~ /[[:alpha:]]/) {
    # one letter variable
      if ($ex[$i] =~ /[abdjmyw]/) { push @rpn, $ex[$i++] }
      else { $err = ['illegal_var', $ex[$i]] }
    }
    elsif ($ex[$i] =~ /[[:alpha:]]/) {
    # long variable - probably month or week day
      my $lvar;
      while ($i < @ex  && $ex[$i] =~ /[[:alpha:]]/) {
	$lvar .= $ex[$i];
	$i++;
      }
      # month and week day names can be used only after the proper variable
      if ($rpn[$#rpn] eq 'm') {
	my $parsed_month = When::parse_month_name($lvar);
	if (!$parsed_month) {$err = ['illegal_month_in_expression',$lvar]}
	$lvar = $parsed_month;
      }
      elsif ($rpn[$#rpn] eq 'w') {
	my $r = When::parse_wday_name($lvar);
	if ($r->{'err'}) {$err = [$r->{'err'},$lvar,
				      $wday_name{$preferences{'language'}}] }
	$lvar = $r->{'match'};
      }
      else {
	return [['expression_syntax_error',$source],undef];
      }
      push @rpn, $lvar;
    }
    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;
}

sub parse_blank_delimited {
  my $date = shift;
  chomp $date;
  my @a = split / +/,$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]>2100) {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])]}
  if ($a[2]<1 || $a[2]>31) {return [main::w('illegal_day_of_month',$a[2])]}
  return ['',When->new(@a)];
}

# can be number or name
# if it's a name, ignores case, trailing dot, and extra characters not needed for uniqueness
sub parse_month_name {
  my $name = shift;
  if ($name =~ m/\d+/) {return $name}
  $name = UnicodeTools::filter_out_accents($name);
  $name =~ s/\.$//; # remove trailing dot
  my $language = $preferences{'language'};
  my @try_langs = ();
  push @try_langs,$language;
  if ($language ne 'en') {
    push @try_langs,'en';
  }
  my @matches = ();
  my $n_matches = 0;
  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) {push @matches,$m; ++$n_matches}
    }
    if ($n_matches==1) {return $matches[0]}
    if ($n_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 ($language ne 'en') {
    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 $names;
  if (@_) {
    my $lang = shift;
    $names = $wday_name{$lang}
  }
  else { $names = $wday_name{$preferences{language}} }
  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 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 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->is_leap_year) {
    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;
}

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;

sub filter_out_accents {
  my $x = shift;
  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',
                $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',
                );
  while ($x =~ m/([^a-zA-Z0-9])/g) {
    my $a = $1;
    if (exists $filter{$a}) {
      $b = $filter{$a};
      $x =~ s/$a/$b/g;
    }
  }
  return $x;
}


