summaryrefslogtreecommitdiff
path: root/Functions/Calendar
diff options
context:
space:
mode:
Diffstat (limited to 'Functions/Calendar')
-rw-r--r--Functions/Calendar/.distfiles6
-rw-r--r--Functions/Calendar/age73
-rw-r--r--Functions/Calendar/calendar356
-rw-r--r--Functions/Calendar/calendar_add69
-rw-r--r--Functions/Calendar/calendar_lockfiles43
-rw-r--r--Functions/Calendar/calendar_read35
-rw-r--r--Functions/Calendar/calendar_scandate519
-rw-r--r--Functions/Calendar/calendar_show24
-rw-r--r--Functions/Calendar/calendar_sort67
9 files changed, 1192 insertions, 0 deletions
diff --git a/Functions/Calendar/.distfiles b/Functions/Calendar/.distfiles
new file mode 100644
index 000000000..5d96b5706
--- /dev/null
+++ b/Functions/Calendar/.distfiles
@@ -0,0 +1,6 @@
+DISTFILES_SRC='
+ .distfiles
+ age
+ calendar calendar_add calendar_lockfiles calendar_read
+ calendar_scandate calendar_show calendar_sort
+'
diff --git a/Functions/Calendar/age b/Functions/Calendar/age
new file mode 100644
index 000000000..4ed3bd8c2
--- /dev/null
+++ b/Functions/Calendar/age
@@ -0,0 +1,73 @@
+# Match the age of a file, for use as a glob qualifer. Can
+# take one or two arguments, which can be supplied by one of two
+# ways (always the same for both arguments):
+#
+# print *(e:age 2006/10/04 2006/10/09:)
+#
+# Match all files modified between the start of those dates.
+#
+# print *(e:age 2006/10/04)
+#
+# Match all files modified on that date. If the second argument is
+# omitted it is taken to be exactly 24 hours after the first argument
+# (even if the first argument contains a time).
+#
+# print *(e:age 2006/10/04:10:15 2006/10/04:10:45)
+#
+# Supply times. All the time and formats handled by calendar_scandate
+# are allowed, but whitespace must be quoted to ensure age receives
+# the correct arguments.
+#
+# AGEREF1=2006/10/04:10:15
+# AGEREF2=2006/10/04:10:45
+# print *(+age)
+#
+# The same example using the other form of argument passing. The
+# dates stay in effect until unset, but will be overridden if
+# any argument is passed in the first format.
+
+emulate -L zsh
+integer mystat disable_stat
+
+zmodload -i zsh/stat
+# Allow the builtin stat to be hidden.
+zmodload -i zsh/parameter
+if [[ $builtins[stat] != defined ]]; then
+ (( disable_stat = 1 ))
+ enable stat
+fi
+
+autoload -U calendar_scandate
+
+local -a vals
+
+[[ -e $REPLY ]] || return 1
+stat -A vals +mtime $REPLY || return 1
+
+if (( $# >= 1 )); then
+ local AGEREF=$1
+ # if 1 argument given, never use globally defined AGEREF2
+ local AGEREF2=$2
+fi
+
+integer mtime=$vals[1] date1 date2
+local REPLY
+
+if calendar_scandate $AGEREF; then
+ date1=$REPLY
+
+ if [[ -n $AGEREF2 ]] && calendar_scandate $AGEREF2; then
+ date2=$REPLY
+ else
+ (( date2 = date1 + 24 * 60 * 60 ))
+ fi
+
+ (( date1 <= mtime && mtime <= date2 ))
+else
+ mystat=1
+fi
+
+# If the builtin stat was previously disabled, disable it again.
+(( disable_stat )) && disable stat
+
+return $mystat
diff --git a/Functions/Calendar/calendar b/Functions/Calendar/calendar
new file mode 100644
index 000000000..124fd9786
--- /dev/null
+++ b/Functions/Calendar/calendar
@@ -0,0 +1,356 @@
+emulate -L zsh
+setopt extendedglob
+
+# standard ctime date/time format
+local ctime="%a %b %d %H:%M:%S %Z %Y"
+
+local line REPLY REPLY2 userange pruned
+local calendar donefile sched newfile warnstr mywarnstr
+integer time start stop today ndays y m d next=-1 shown done nodone
+integer verbose warntime mywarntime t tsched i rstat remaining
+integer showcount icount
+local -a calendar_entries
+local -a times calopts showprog lockfiles match mbegin mend
+
+zmodload -i zsh/datetime || return 1
+zmodload -i zsh/zutil || return 1
+
+autoload -U calendar_{read,scandate,show,lockfiles}
+
+# Read the calendar file from the calendar-file style
+zstyle -s ':datetime:calendar:' calendar-file calendar || calendar=~/calendar
+newfile=$calendar.new.$HOST.$$
+zstyle -s ':datetime:calendar:' done-file donefile || donefile="$calendar.done"
+# Read the programme to show the message from the show-prog style.
+zstyle -a ':datetime:calendar:' show-prog showprog ||
+ showprog=(calendar_show)
+# Amount of time before an event when it should be flagged.
+# May be overridden in individual entries
+zstyle -s ':datetime:calendar:' warn-time warnstr || warnstr="0:05"
+
+if [[ -n $warnstr ]]; then
+ if [[ $warnstr = <-> ]]; then
+ (( warntime = warnstr ))
+ elif ! calendar_scandate -ar $warnstr; then
+ print >&2 \
+ "warn-time value '$warnstr' not understood; using default 5 minutes"
+ warnstr="5 mins"
+ (( warntime = 5 * 60 ))
+ else
+ (( warntime = REPLY ))
+ fi
+fi
+
+[[ -f $calendar ]] || return 1
+
+# We're not using getopts because we want +... to refer to a
+# relative time, not an option, and allow some other additions
+# like handling -<->.
+integer opti=0
+local opt optrest optarg
+
+while [[ ${argv[opti+1]} = -* ]]; do
+ (( opti++ ))
+ opt=${argv[opti][2]}
+ optrest=${argv[opti][3,-1]}
+ [[ -z $opt || $opt = - ]] && break
+ while [[ -n $opt ]]; do
+ case $opt in
+ ########################
+ # Options with arguments
+ ########################
+ ([CnS])
+ if [[ -n $optrest ]]; then
+ optarg=$optrest
+ optrest=
+ elif (( opti < $# )); then
+ optarg=$argv[++opti]
+ optrest=
+ else
+ print -r "$0: option -$opt requires an argument." >&2
+ return 1
+ fi
+ case $opt in
+ (C)
+ # Pick the calendar file, overriding style and default.
+ calendar=$optarg
+ ;;
+
+ (n)
+ # Show this many remaining events regardless of date.
+ showcount=$optarg
+ if (( showcount <= 0 )); then
+ print -r "$0: option -$opt requires a positive integer." >&2
+ return 1
+ fi
+ ;;
+
+ (S)
+ # Explicitly specify a show programme, overriding style and default.
+ # Colons in the argument are turned into space.
+ showprog=(${(s.:.)optarg})
+ ;;
+ esac
+ ;;
+
+ ###########################
+ # Options without arguments
+ ###########################
+ (d)
+ # Move out of date items to the done file.
+ (( done = 1 ))
+ ;;
+
+ (D)
+ # Don't use done; needed with sched
+ (( nodone = 1 ))
+ ;;
+
+ (r)
+ # Show all remaining options in the calendar, i.e.
+ # respect start time but ignore end time.
+ # Any argument is treated as a start time.
+ (( remaining = 1 ))
+ ;;
+
+ (s)
+ # Use the "sched" builtin to scan at the appropriate time.
+ sched=sched
+ (( done = 1 ))
+ ;;
+
+ (v)
+ # Verbose
+ verbose=1
+ ;;
+
+ (<->)
+ # Shorthand for -n <->
+ showcount=$opt
+ ;;
+
+ (*)
+ print "$0: unrecognised option: -$opt" >&2
+ return 1
+ ;;
+ esac
+ opt=$optrest[1]
+ optrest=$optrest[2,-1]
+ done
+done
+calopts=($argv[1,opti])
+shift $(( opti ))
+
+# Use of donefile requires explicit or implicit option request, plus
+# no explicit -D. It may already be empty because of the style.
+(( done && !nodone )) || donefile=
+
+if (( $# > 1 || ($# == 1 && remaining) )); then
+ if [[ $1 = now ]]; then
+ start=$EPOCHSECONDS
+ elif [[ $1 = <-> ]]; then
+ start=$1
+ else
+ if ! calendar_scandate -a $1; then
+ print "$0: failed to parse date/time: $1" >&2
+ return 1
+ fi
+ start=$REPLY
+ fi
+ shift
+else
+ # Get the time at which today started.
+ y=${(%):-"%D{%Y}"} m=${(%):-"%D{%m}"} d=${(%):-"%D{%d}"}
+ strftime -s today -r "%Y/%m/%d" "$y/$m/$d"
+ start=$today
+fi
+# day of week of start time
+strftime -s wd "%u" $start
+
+if (( $# && !remaining )); then
+ if [[ $1 = +* ]]; then
+ if ! calendar_scandate -ar ${1[2,-1]}; then
+ print "$0: failed to parse relative time: $1" >&2
+ return 1
+ fi
+ (( stop = start + REPLY ))
+ elif [[ $1 = <-> ]]; then
+ stop=$1
+ else
+ if ! calendar_scandate -a $1; then
+ print "$0: failed to parse date/time: $1" >&2
+ return 1
+ fi
+ stop=$REPLY
+ fi
+ if (( stop < start )); then
+ strftime -s REPLY $ctime $start
+ strftime -s REPLY2 $ctime $stop
+ print "$0: requested end time is before start time:
+ start: $REPLY
+ end: $REPLY2" >&2
+ return 1
+ fi
+ shift
+else
+ # By default, show 2 days. If it's Friday (5) show up to end
+ # of Monday (4) days; likewise on Saturday show 3 days.
+ # If -r, this is calculated but not used. This is paranoia,
+ # to avoid an unusable value of stop; but it shouldn't get used.
+ case $wd in
+ (5)
+ ndays=4
+ ;;
+
+ (6)
+ ndays=3
+ ;;
+
+ (*)
+ ndays=2
+ ;;
+ esac
+ stop=$(( start + ndays * 24 * 60 * 60 ))
+fi
+
+if (( $# )); then
+ print "Usage: $0 [ start-date-time stop-date-time ]" >&2
+ return 1
+fi
+
+autoload -Uz matchdate
+
+[[ -n $donefile ]] && rm -f $newfile
+
+if (( verbose )); then
+ print -n "start: "
+ strftime $ctime $start
+ print -n "stop: "
+ if (( remaining )); then
+ print "none"
+ else
+ strftime $ctime $stop
+ fi
+fi
+
+# start of block for following always to clear up lockfiles.
+{
+ if [[ -n $donefile ]]; then
+ # Attempt to lock both $donefile and $calendar.
+ # Don't lock $newfile; we've tried our best to make
+ # the name unique.
+ calendar_lockfiles $calendar $donefile || return 1
+ fi
+
+ calendar_read $calendar
+ for line in $calendar_entries; do
+ # This call sets REPLY to the date and time in seconds since the epoch,
+ # REPLY2 to the line with the date and time removed.
+ calendar_scandate -as $line || continue
+ (( t = REPLY ))
+
+ # Look for specific warn time.
+ pruned=${REPLY2#(|*[[:space:],])WARN[[:space:]]}
+ (( mywarntime = warntime ))
+ mywarnstr=$warnstr
+ if [[ $pruned != $REPLY2 ]]; then
+ if calendar_scandate -ars $pruned; then
+ (( mywarntime = REPLY ))
+ mywarnstr=${pruned%%"$REPLY2"}
+ fi
+ fi
+
+ if (( verbose )); then
+ print "Examining: $line"
+ print -n " Date/time: "
+ strftime $ctime $t
+ if [[ -n $sched ]]; then
+ print " Warning $mywarntime seconds ($mywarnstr) before"
+ fi
+ fi
+ (( shown = 0 ))
+ if (( t >= start && (remaining || t <= stop || icount < showcount) ))
+ then
+ $showprog $start $stop "$line"
+ (( shown = 1, icount++ ))
+ elif [[ -n $sched ]]; then
+ (( tsched = t - mywarntime ))
+ if (( tsched >= start && tsched <= stop)); then
+ $showprog $start $stop "due in ${mywarnstr}: $line"
+ fi
+ fi
+ if [[ -n $sched ]]; then
+ if (( t - mywarntime > EPOCHSECONDS )); then
+ # schedule for a warning
+ (( tsched = t - mywarntime ))
+ else
+ # schedule for event itself
+ (( tsched = t ))
+ fi
+ if (( (tsched > EPOCHSECONDS || ! shown) &&
+ (next < 0 || tsched < next) )); then
+ (( next = tsched ))
+ fi
+ fi
+ if [[ -n $donefile ]]; then
+ if (( t <= EPOCHSECONDS && shown )); then
+ # Done and dusted.
+ # TODO: handle repeated times from REPLY2.
+ if ! print -r $line >>$donefile; then
+ if (( done != 3 )); then
+ (( done = 3 ))
+ print "Failed to append to $donefile" >&2
+ fi
+ elif (( done != 3 )); then
+ (( done = 2 ))
+ fi
+ else
+ # Still not over.
+ if ! print -r $line >>$newfile; then
+ if (( done != 3 )); then
+ (( done = 3 ))
+ print "Failed to append to $newfile" >&2
+ fi
+ elif (( done != 3 )); then
+ (( done = 2 ))
+ fi
+ fi
+ fi
+ done
+
+ if [[ -n $sched ]]; then
+ if [[ $next -ge 0 ]]; then
+ # Remove any existing calendar scheduling.
+ # Luckily sched doesn't delete its schedule in a subshell.
+ sched | while read line; do
+ if [[ $line = (#b)[[:space:]]#(<->)[[:space:]]##*[[:space:]]'calendar -s'* ]]; then
+ # End of pipeline run in current shell, so delete directly.
+ sched -1 $match[1]
+ fi
+ done
+ $sched $next calendar "${calopts[@]}" $next $next
+ else
+ $showprog $start $stop \
+"No more calendar events: calendar not rescheduled.
+Run \"calendar -s\" again if you add to it."
+ fi
+ fi
+
+ if (( done == 2 )); then
+ if ! mv $calendar $calendar.old; then
+ print "Couldn't back up $calendar to $calendar.old.
+New calendar left in $newfile." >&2
+ (( rstat = 1 ))
+ elif ! mv $newfile $calendar; then
+ print "Failed to rename $newfile to $calendar.
+Old calendar left in $calendar.old." >&2
+ (( rstat = 1 ))
+ fi
+ elif [[ -n $donefile ]]; then
+ rm -f $newfile
+ fi
+} always {
+ (( ${#lockfiles} )) && rm -f $lockfiles
+}
+
+return $rstat
diff --git a/Functions/Calendar/calendar_add b/Functions/Calendar/calendar_add
new file mode 100644
index 000000000..2a00811fd
--- /dev/null
+++ b/Functions/Calendar/calendar_add
@@ -0,0 +1,69 @@
+#!/bin/env zsh
+
+# All arguments are joined with spaces and inserted into the calendar
+# file at the appropriate point.
+#
+# While the function compares the date of the new entry with dates in the
+# existing calendar file, it does not do any sorting; it inserts the new
+# entry before the first existing entry with a later date and time.
+
+emulate -L zsh
+setopt extendedglob
+
+local calendar newfile REPLY lastline
+local -a calendar_entries lockfiles
+integer newdate done rstat
+
+autoload -U calendar_{read,lockfiles}
+
+# Read the calendar file from the calendar-file style
+zstyle -s ':datetime:calendar_add:' calendar-file calendar ||
+ calendar=~/calendar
+newfile=$calendar.new.$HOST.$$
+
+if ! calendar_scandate -a "$*"; then
+ print "$0: failed to parse date/time" >&2
+ return 1
+fi
+(( newdate = $REPLY ))
+
+# $calendar doesn't necessarily exist yet.
+
+# start of block for following always to clear up lockfiles.
+{
+ calendar_lockfiles $calendar || return 1
+
+ if [[ -f $calendar ]]; then
+ calendar_read $calendar
+
+ {
+ for line in $calendar_entries; do
+ if (( ! done )) && calendar_scandate -a $line && (( REPLY > newdate )); then
+ print -r -- "$*"
+ (( done = 1 ))
+ elif [[ $REPLY -eq $newdate && $line = "$*" ]]; then
+ (( done = 1 ))
+ fi
+ print -r -- $line
+ done
+ (( done )) || print -r -- "$*"
+ } >$newfile
+ if ! mv $calendar $calendar.old; then
+ print "Couldn't back up $calendar to $calendar.old.
+New calendar left in $newfile." >&2
+ (( rstat = 1 ))
+ fi
+ else
+ print -r -- $line >$newfile
+ fi
+
+ if (( !rstat )) && ! mv $newfile $calendar; then
+ print "Failed to rename $newfile to $calendar.
+Old calendar left in $calendar.old." >&2
+ (( rstat = 1 ))
+ fi
+} always {
+ (( ${#lockfiles} )) && rm -f $lockfiles
+}
+
+return $rstat
diff --git a/Functions/Calendar/calendar_lockfiles b/Functions/Calendar/calendar_lockfiles
new file mode 100644
index 000000000..58ee42114
--- /dev/null
+++ b/Functions/Calendar/calendar_lockfiles
@@ -0,0 +1,43 @@
+# Lock the given files.
+# Append the names of lockfiles to the array lockfiles.
+
+local file lockfile msgdone
+# Number of attempts to lock a file. Probably not worth stylising.
+integer lockattempts=3
+
+# The lockfile name is not stylised: it has to be a fixed
+# derivative of the main fail.
+for file; do
+ lockfile=$file.lockfile
+ for (( i = 0; i < lockattempts; i++ )); do
+ if ln -s $file $lockfile >/dev/null 2>&1; then
+ lockfiles+=($lockfile)
+ break
+ fi
+ if zle && [[ -z $msgdone ]]; then
+ msgdone="${lockfile}: waiting to acquire lock"
+ zle -M $msgdone
+ fi
+ sleep 1
+ done
+ if [[ -n $msgdone ]]; then
+ zle -M ${msgdone//?/ }
+ msgdone=
+ fi
+ if [[ ${lockfiles[-1]} != $lockfile ]]; then
+ msgdone="Failed to lock $file; giving up after $lockattempts attempts.
+Another instance of calendar may be using it.
+Delete $lockfiles if you believe this to be an error."
+ if zle; then
+ zle -M $msgdone
+ else
+ print $msgdone >&2
+ fi
+ # The parent should take action to delete any lockfiles
+ # already locked. Typically this won't be necessary, since
+ # we will always lock the main calendar file first.
+ return 1
+ fi
+done
+
+return 0
diff --git a/Functions/Calendar/calendar_read b/Functions/Calendar/calendar_read
new file mode 100644
index 000000000..ed163887f
--- /dev/null
+++ b/Functions/Calendar/calendar_read
@@ -0,0 +1,35 @@
+# Utility for "calendar" to read entries into the array calendar_entries.
+# This should be local to the caller.
+# The only argument is the file to read. We expect options etc. to
+# be correct.
+#
+# This is based on Emacs calendar syntax, which has two implications:
+# - Lines beginning with whitespace are continuation lines.
+# Hence we have to read the entire file first to determine entries.
+# - Lines beginning with "&" are inhibited from producing marks in
+# Emacs calendar window. This is irrelevant to us, so we
+# we simply remove leading ampersands. This is necessary since
+# we expect the date to start at the beginning of the line.
+#
+# TODO: Emacs has some special handling for entries where the first line
+# has only the date and continuation lines indicate times. Actually,
+# it doesn't parse the times as far as I can see, but if we want to
+# handle that format sensibly we would need to here. It could
+# be tricky to get right.
+
+local calendar=$1 line
+local -a lines
+
+lines=(${(f)"$(<$calendar)"})
+
+calendar_entries=()
+# ignore blank lines
+for line in $lines; do
+ if [[ $line = [[:space:]]* ]]; then
+ if (( ${#calendar_entries} )); then
+ calendar_entries[-1]+=$'\n'$line
+ fi
+ else
+ calendar_entries+=(${line##\&})
+ fi
+done
diff --git a/Functions/Calendar/calendar_scandate b/Functions/Calendar/calendar_scandate
new file mode 100644
index 000000000..f0024b89a
--- /dev/null
+++ b/Functions/Calendar/calendar_scandate
@@ -0,0 +1,519 @@
+# Scan a line for various common date and time formats.
+# Set REPLY to the number of seconds since the epoch at which that
+# time occurs. The time does not need to be matched; this will
+# produce midnight at the start of the date.
+#
+# Absolute times
+#
+# The rules below are fairly complicated, to allow any natural (and
+# some highly unnatural but nonetheless common) combination of
+# time and date used by English speakers. It is recommended that,
+# rather than exploring the intricacies of the system, users find
+# a date format that is natural to them and stick to it. This
+# will avoid unexpected effects. Various key facts should be noted,
+# explained in more detail below:
+#
+# - In particular, note the confusion between month/day/year and
+# day/month/year when the month is numeric; this format should be
+# avoided if at all possible. Many alternatives are available.
+# - However, there is currently no localization support, so month
+# names must be English (though only the first three letters are required).
+# The same applies to days of the week if they occur (they are not useful).
+# - The year must be given in full to avoid confusion, and only years
+# from 1900 to 2099 inclusive are matched.
+# - Although timezones are parsed (complicated formats may not be recognized),
+# they are then ignored; no time adjustment is made.
+#
+# The following give some obvious examples; users finding here
+# a format they like and not subject to vagaries of style may skip
+# the full description. As dates and times are matched separately
+# (even though the time may be embedded in the date), any date format
+# may be mixed with any format for the time of day provide the
+# separators are clear (whitespace, colons, commas).
+# 2007/04/03 13:13
+# 2007/04/03:13:13
+# 2007/04/03 1:13 pm
+# 3rd April 2007, 13:13
+# April 3rd 2007 1:13 p.m.
+# Apr 3, 2007 13:13
+# Tue Apr 03 13:13:00 2007
+# 13:13 2007/apr/3
+#
+# Times are parsed and extracted before dates. They must use colons
+# to separate hours and minutes, though a dot is allowed before seconds
+# if they are present. This limits time formats to
+# HH:MM[:SS[.FFFFF]] [am|pm|a.m.|p.m.]
+# HH:MM.SS[.FFFFF] [am|pm|a.m.|p.m.]
+# in which square brackets indicate optional elements, possibly with
+# alternatives. Fractions of a second are recognised but ignored.
+# Unless -r is given (see below), a date is mandatory but a time of day is
+# not; the time returned is at the start of the date.
+#
+# Time zones are not handled, though if one is matched following a time
+# specification it will be removed to allow a surrounding date to be
+# parsed. This only happens if the format of the timezone is not too
+# wacky:
+# +0100
+# GMT
+# GMT-7
+# CET+1CDT
+# etc. are all understood, but any part of the timezone that is not numeric
+# must have exactly three capital letters in the name.
+#
+# Dates suffer from the ambiguity between DD/MM/YYYY and MM/DD/YYYY. It is
+# recommended this form is avoided with purely numeric dates, but use of
+# ordinals, eg. 3rd/04/2007, will resolve the ambiguity as the ordinal is
+# always parsed as the day of the month. Years must be four digits (and
+# the first two must be 19 or 20); 03/04/08 is not recognised. Other
+# numbers may have leading zeroes, but they are not required. The
+# following are handled:
+# YYYY/MM/DD
+# YYYY-MM-DD
+# YYYY/MNM/DD
+# YYYY-MNM-DD
+# DD[th|st|rd] MNM[,] YYYY
+# DD[th|st|rd] MNM[,] current year assumed
+# MNM DD[th|st|rd][,] YYYY
+# MNM DD[th|st|rd][,] current year assumed
+# DD[th|st|rd]/MM[,] YYYY
+# DD[th|st|rd]/MM/YYYY
+# MM/DD[th|st|rd][,] YYYY
+# MM/DD[th|st|rd]/YYYY
+# Here, MNM is at least the first three letters of a month name,
+# matched case-insensitively. The remainder of the month name may appear but
+# its contents are irrelevant, so janissary, febrile, martial, apricot,
+# etc. are happily handled.
+#
+# Note there are only two cases that assume the current year, the
+# form "Jun 20" or "14 September" (the only two commonly occurring
+# forms, apart from a "the" in some forms of English, which isn't
+# currently supported). Such dates will of course become ambiguous
+# in the future, so should ideally be avoided.
+#
+# Times may follow dates with a colon, e.g. 1965/07/12:09:45; this
+# is in order to provide a format with no whitespace. A comma
+# and whitespace are allowed, e.g. "1965/07/12, 09:45".
+# Currently the order of these separators is not checked, so
+# illogical formats such as "1965/07/12, : ,09:45" will also
+# be matched. Otherwise, a time is only recognised as being associated
+# with a date if there is only whitespace in between, or if the time
+# was embedded in the date.
+#
+# Days of the week are not scanned, but will be ignored if they occur
+# at the start of the date pattern only.
+#
+# For example, the standard date format:
+# Fri Aug 18 17:00:48 BST 2006
+# is handled by matching HH:MM:SS and removing it together with the
+# matched (but unused) time zone. This leaves the following:
+# Fri Aug 18 2006
+# "Fri" is ignored and the rest is matched according to the sixth of
+# the standard rules.
+#
+# Relative times
+# ==============
+#
+# The option -r allows a relative time. Years (or ys, yrs, or without s),
+# months (or mths, mons, mnths, months, or without s --- "m", "ms" and
+# "mns" are ambiguous and are not handled), weeks (or ws, wks, or without
+# s) and days (or ds, dys, days, or without s), hours (or hs, hrs, with or
+# without s), minutes (or mins, with or without s) and seconds (or ss,
+# secs, with or without s) are understood. Spaces between the numbers
+# are optional, but are required between items, although a comma
+# may be used (with or without spaces).
+#
+# Note that a year here is 365.25 days and a month is 30 days. TODO:
+# improve this by passing down base time and adjusting. (This will
+# be crucial for events repeating monthly.) TODO: it then makes
+# sense to make PERIODly = 1 PERIOD (also for PERIOD = dai!)
+#
+# This allows forms like:
+# 30 years 3 months 4 days 3:42:41
+# 14 days 5 hours
+# 4d,10hr
+# In this case absolute dates are ignored.
+
+emulate -L zsh
+setopt extendedglob
+
+zmodload -i zsh/datetime || return 1
+
+# separator characters before time or between time and date
+# allow , - or : before the time: this allows spaceless but still
+# relatively logical dates like 2006/09/19:14:27
+# don't allow / before time ! the above
+# is not 19 hours 14 mins and 27 seconds after anything.
+local tschars="[-,:[:space:]]"
+# start pattern for time when anchored
+local tspat_anchor="(${tschars}#)"
+# ... when not anchored
+local tspat_noanchor="(|*${tschars})"
+# separator characters between elements. comma is fairly
+# natural punctuation; otherwise only allow whitespace.
+local schars="[.,[:space:]]"
+local daypat="${schars}#(sun|mon|tue|wed|thu|fri|sat)[a-z]#${schars}#"
+# Start pattern for date: treat , as space for simplicity. This
+# is illogical at the start but saves lots of minor fiddling later.
+# Date start pattern when anchored at the start.
+# We need to be able to ignore the day here, although (for consistency
+# with the unanchored case) we don't remove it until later.
+# (The problem in the other case is that matching anything before
+# the day of the week is greedy, so the day of the week gets ignored
+# if it's optional.)
+local dspat_anchor="(|(#B)${daypat}(#b)${schars}#)"
+# Date start pattern when not anchored at the start.
+local dspat_noanchor="(|*${schars})"
+# end pattern for relative times: similar remark about use of $schars.
+local repat="(|s)(|${schars}*)"
+# not locale-dependent! I don't know how to get the months out
+# of the system for the purpose of finding out where they occur.
+# We may need some completely different heuristic.
+local monthpat="(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]#"
+# days, not handled but we need to ignore them. also not localized.
+
+integer year month day hour minute second
+local opt line orig_line mname MATCH MBEGIN MEND tz
+local -a match mbegin mend
+# Flags that we found a date or a time (maybe a relative time)
+integer date_found time_found
+# Indices of positions of start and end of time and dates found.
+# These are actual character indices as zsh would normally use, i.e.
+# line[time_start,time_end] is the string for the time.
+integer time_start time_end date_start date_end
+integer anchor anchor_end debug relative reladd setvar
+
+while getopts "aAdrs" opt; do
+ case $opt in
+ (a)
+ # anchor
+ (( anchor = 1 ))
+ ;;
+
+ (A)
+ # anchor at end, too
+ (( anchor = 1, anchor_end = 1 ))
+ ;;
+
+ (d)
+ # enable debug output
+ (( debug = 1 ))
+ ;;
+
+ (r)
+ (( relative = 1 ))
+ ;;
+
+ (s)
+ (( setvar = 1 ))
+ ;;
+
+ (*)
+ return 1
+ ;;
+ esac
+done
+shift $(( OPTIND - 1 ))
+
+line=$1 orig_line=$1
+
+local dspat tspat
+if (( anchor )); then
+ # Anchored at the start.
+ dspat=$dspat_anchor
+ if (( relative )); then
+ tspat=$tspat_anchor
+ else
+ # We'll test later if the time is associated with the date.
+ tspat=$tspat_noanchor
+ fi
+else
+ dspat=$dspat_noanchor
+ tspat=$tspat_noanchor
+fi
+
+# Look for a time separately; we need colons for this.
+case $line in
+ # with seconds, am/pm: don't match / in front.
+ ((#ibm)${~tspat}(<0-12>):(<0-59>)[.:]((<0-59>)(.<->|))[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))(*))
+ hour=$match[2]
+ minute=$match[3]
+ second=$match[5]
+ [[ $match[7] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
+ time_found=1
+ ;;
+
+ # no seconds, am/pm
+ ((#ibm)${~tspat}(<0-12>):(<0-59>)[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))(*))
+ hour=$match[2]
+ minute=$match[3]
+ [[ $match[4] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
+ time_found=1
+ ;;
+
+ # no colon, even, but a.m./p.m. indicator
+ ((#ibm)${~tspat}(<0-12>)[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))(*))
+ hour=$match[2]
+ minute=0
+ [[ $match[3] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
+ time_found=1
+ ;;
+
+ # 24 hour clock, with seconds
+ ((#ibm)${~tspat}(<0-24>):(<0-59>)[.:]((<0-59>)(.<->|))(*))
+ hour=$match[2]
+ minute=$match[3]
+ second=$match[5]
+ time_found=1
+ ;;
+
+ # 24 hour clock, no seconds
+ ((#ibm)${~tspat}(<0-24>):(<0-59>)(*))
+ hour=$match[2]
+ minute=$match[3]
+ time_found=1
+ ;;
+esac
+
+(( hour == 24 )) && hour=0
+
+if (( time_found )); then
+ # time was found
+ time_start=$mbegin[2]
+ time_end=$mend[-2]
+ # Remove the timespec because it may be in the middle of
+ # the date (as in the output of "date".
+ # There may be a time zone, too, which we don't yet handle.
+ # (It's not in POSIX strptime() and libraries don't support it well.)
+ # This attempts to remove some of the weirder forms.
+ if [[ $line[$time_end+1,-1] = (#b)[[:space:]]#([A-Z][A-Z][A-Z]|[-+][0-9][0-9][0-9][0-9])([[:space:]]|(#e))* || \
+ $line[$time_end+1,-1] = (#b)[[:space:]]#([A-Z][A-Z][A-Z](|[-+])<0-12>)([[:space:]]|(#e))* || \
+ $line[$time_end+1,-1] = (#b)[[:space:]]#([A-Z][A-Z][A-Z](|[-+])<0-12>[A-Z][A-Z][A-Z])([[:space:]]|(#e))* ]]; then
+ (( time_end += ${mend[-1]} ))
+ tz=$match[1]
+ fi
+ line=$line[1,time_start-1]$line[time_end+1,-1]
+ (( debug )) && print "line after time: $line"
+fi
+
+if (( relative == 0 )); then
+ # Date.
+ case $line in
+ # Look for YEAR[-/.]MONTH[-/.]DAY
+ ((#bi)${~dspat}((19|20)[0-9][0-9])[-/](<1-12>)[-/](<1-31>)*)
+ year=$match[2]
+ month=$match[4]
+ day=$match[5]
+ date_start=$mbegin[2] date_end=$mend[5]
+ date_found=1
+ ;;
+
+ # Same with month name
+ ((#bi)${~dspat}((19|20)[0-9][0-9])[-/]${~monthpat}[-/](<1-31>)*)
+ year=$match[2]
+ mname=$match[4]
+ day=$match[5]
+ date_start=$mbegin[2] date_end=$mend[5]
+ date_found=1
+ ;;
+
+ # Look for DAY[th/st/rd] MNAME[,] YEAR
+ ((#bi)${~dspat}(<1-31>)(|th|st|rd)[[:space:]]##${~monthpat}(|,)[[:space:]]##((19|20)[0-9][0-9])*)
+ day=$match[2]
+ mname=$match[4]
+ year=$match[6]
+ date_start=$mbegin[2] date_end=$mend[6]
+ date_found=1
+ ;;
+
+ # Look for MNAME DAY[th/st/rd][,] YEAR
+ ((#bi)${~dspat}${~monthpat}[[:space:]]##(<1-31>)(|th|st|rd)(|,)[[:space:]]##((19|20)[0-9][0-9])*)
+ mname=$match[2]
+ day=$match[3]
+ year=$match[6]
+ date_start=$mbegin[2] date_end=$mend[6]
+ date_found=1
+ ;;
+
+ # Look for DAY[th/st/rd] MNAME; assume current year
+ ((#bi)${~dspat}(<1-31>)(|th|st|rd)[[:space:]]##${~monthpat}(|,)([[:space:]]##*|))
+ day=$match[2]
+ mname=$match[4]
+ strftime -s year "%Y" $EPOCHSECONDS
+ date_start=$mbegin[2] date_end=$mend[5]
+ date_found=1
+ ;;
+
+ # Look for MNAME DAY[th/st/rd]; assume current year
+ ((#bi)${~dspat}${~monthpat}[[:space:]]##(<1-31>)(|th|st|rd)(|,)([[:space:]]##*|))
+ mname=$match[2]
+ day=$match[3]
+ strftime -s year "%Y" $EPOCHSECONDS
+ date_start=$mbegin[2] date_end=$mend[5]
+ date_found=1
+ ;;
+
+ # Now it gets a bit ambiguous.
+ # Look for DAY[th/st/rd][/]MONTH[/ ,]YEAR
+ ((#bi)${~dspat}(<1-31>)(|th|st|rd)/(<1-12>)((|,)[[:space:]]##|/)((19|20)[0-9][0-9])*)
+ day=$match[2]
+ month=$match[4]
+ year=$match[7]
+ date_start=$mbegin[2] date_end=$mend[7]
+ date_found=1
+ ;;
+
+ # Look for MONTH[/]DAY[th/st/rd][/ ,]YEAR
+ ((#bi)${~dspat}(<1-12>)/(<1-31>)(|th|st|rd)((|,)[[:space:]]##|/)((19|20)[0-9][0-9])*)
+ month=$match[2]
+ day=$match[3]
+ year=$match[7]
+ date_start=$mbegin[2] date_end=$mend[7]
+ date_found=1
+ ;;
+ esac
+fi
+
+if (( date_found )); then
+ # date found
+ # see if there's a day at the start
+ if [[ ${line[1,$date_start-1]} = (#bi)${~daypat} ]]; then
+ date_start=$mbegin[1]
+ fi
+ line=${line[1,$date_start-1]}${line[$date_end+1,-1]}
+ if (( time_found )); then
+ # If we found a time, it must be associated with the date,
+ # or we can't use it. Since we removed the time from the
+ # string to find the date, however, it's complicated to
+ # know where both were found. Reconstruct the date indices of
+ # the original string.
+ if (( time_start <= date_start )); then
+ # Time came before start of date; add length in.
+ (( date_start += time_end - time_start + 1 ))
+ fi
+ if (( time_start <= date_end )); then
+ (( date_end += time_end - time_start + 1 ))
+ fi
+
+ if (( time_end + 1 < date_start )); then
+ # If time wholly before date, OK if only separator characters
+ # in between. (This allows some illogical stuff with commas
+ # but that's probably not important.)
+ if [[ ${orig_line[time_end+1,date_start-1]} != ${~schars}# ]]; then
+ # Clearly this can't work if anchor is set. In principle,
+ # we could match the date and ignore the time if it wasn't.
+ # However, that seems dodgy.
+ return 1
+ else
+ # Form massaged line by removing the entire date/time chunk.
+ line="${orig_line[1,time_start-1]}${orig_line[date_end+1,-1]}"
+ fi
+ elif (( date_end + 1 < time_start )); then
+ # If date wholly before time, OK if only time separator characters
+ # in between. This allows 2006/10/12:13:43 etc.
+ if [[ ${orig_line[date_end+1,time_start-1]} != ${~tschars}# ]]; then
+ # Here, we assume the time is associated with something later
+ # in the line. This is pretty much inevitable for the sort
+ # of use we are expecting. For example,
+ # 2006/10/24 Meeting from early, may go on till 12:00.
+ # or with some uses of the calendar system,
+ # 2006/10/24 MR 1 Another pointless meeting WARN 01:00
+ # The 01:00 says warn an hour before, not that the meeting starts
+ # at 1 am. About the only safe way round would be to force
+ # a time to be present, but that's not how the traditional
+ # calendar programme works.
+ #
+ # Hence we need to reconstruct.
+ (( time_found = 0, hour = 0, minute = 0, second = 0 ))
+ line="${orig_line[1,date_start-1]}${orig_line[date_end+1,-1]}"
+ else
+ # As above.
+ line="${orig_line[1,date_start-1]}${orig_line[time_end+1,-1]}"
+ fi
+ fi
+ if (( debug )); then
+ print "Time string: $time_start,$time_end:" \
+ "'$orig_line[time_start,time_end]'"
+ print "Date string: $date_start,$date_end:" \
+ "'$orig_line[date_start,date_end]'"
+ print "Remaining line: '$line'"
+ fi
+ fi
+fi
+
+if (( relative )); then
+ if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(y|yr|year)${~repat} ]]; then
+ (( reladd += ((365*4+1) * 24 * 60 * 60 * ${match[2]} + 1) / 4 ))
+ line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
+ time_found=1
+ fi
+ if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(mth|mon|mnth|month)${~repat} ]]; then
+ (( reladd += 30 * 24 * 60 * 60 * ${match[2]} ))
+ line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
+ time_found=1
+ fi
+ if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(w|wk|week)${~repat} ]]; then
+ (( reladd += 7 * 24 * 60 * 60 * ${match[2]} ))
+ line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
+ time_found=1
+ fi
+ if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(d|dy|day)${~repat} ]]; then
+ (( reladd += 24 * 60 * 60 * ${match[2]} ))
+ line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
+ time_found=1
+ fi
+ if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(h|hr|hour)${~repat} ]]; then
+ (( reladd += 60 * 60 * ${match[2]} ))
+ line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
+ time_found=1
+ fi
+ if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(min|minute)${~repat} ]]; then
+ (( reladd += 60 * ${match[2]} ))
+ line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
+ time_found=1
+ fi
+ if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(s|sec|second)${~repat} ]]; then
+ (( reladd += ${match[2]} ))
+ line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
+ time_found=1
+ fi
+fi
+
+if (( relative )); then
+ # If no date was found, we're in trouble unless we found a time.
+ if (( time_found )); then
+ if (( anchor_end )); then
+ # must be left with only separator characters
+ if [[ $line != ${~schars}# ]]; then
+ return 1
+ fi
+ fi
+ (( REPLY = reladd + (hour * 60 + minute) * 60 + second ))
+ [[ -n $setvar ]] && REPLY2=$line
+ return 0
+ fi
+ return 1
+elif (( ! date_found )); then
+ return 1
+fi
+
+if (( anchor_end )); then
+ # must be left with only separator characters
+ if [[ $line != ${~schars}# ]]; then
+ return 1
+ fi
+fi
+
+local fmt nums
+if [[ -n $mname ]]; then
+ fmt="%Y %b %d %H %M %S"
+ nums="$year $mname $day $hour $minute $second"
+else
+ fmt="%Y %m %d %H %M %S"
+ nums="$year $month $day $hour $minute $second"
+fi
+
+strftime -s REPLY -r $fmt $nums
+
+[[ -n $setvar ]] && REPLY2=$line
+
+return 0
diff --git a/Functions/Calendar/calendar_show b/Functions/Calendar/calendar_show
new file mode 100644
index 000000000..f731d07a5
--- /dev/null
+++ b/Functions/Calendar/calendar_show
@@ -0,0 +1,24 @@
+integer start=$1 stop=$2
+shift 2
+
+[[ -o zle ]] && zle -I
+print -r "$*"
+
+local -a cmd
+zmodload -i zsh/parameter || return
+
+# Use xmessage to display the message if the start and stop time
+# are the same, indicating we have been scheduled to display it.
+# Don't do this if there's already an xmessage for the same user.
+# HERE: this should be configurable and we should be able to do
+# better if xmessage isn't available, e.g. wish.
+if [[ -n $DISPLAY && $start -eq $stop ]]; then
+ if [[ -n ${commands[xmessage]} ]]; then
+ cmd=(xmessage -center)
+ fi
+ if [[ -n $cmd[0] ]] &&
+ ! ps -u$UID | grep $cmd[0] >/dev/null 2>&1; then
+ # turn off job control for this
+ ($cmd "$*" &)
+ fi
+fi
diff --git a/Functions/Calendar/calendar_sort b/Functions/Calendar/calendar_sort
new file mode 100644
index 000000000..7d346efc1
--- /dev/null
+++ b/Functions/Calendar/calendar_sort
@@ -0,0 +1,67 @@
+emulate -L zsh
+setopt extendedglob
+
+autoload -U calendar_{read,scandate,lockfiles}
+
+local calendar line REPLY new lockfile
+local -a calendar_entries
+local -a times lines_sorted lines_unsorted lines_failed lockfiles
+integer i
+
+# Read the calendar file from the calendar-file style
+zstyle -s ':datetime:calendar:' calendar-file calendar || calendar=~/calendar
+
+# Start block for "always" to handle lockfile
+{
+ calendar_lockfiles $calendar || return 1
+
+ new=$calendar.new.$$
+ calendar_read $calendar
+ if [[ ${#calendar_entries} -eq 0 || \
+ ( ${#calendar_entries} -eq 1 && -z $calendar_entries[1] ) ]]; then
+ return 0
+ fi
+
+ for line in $calendar_entries; do
+ if calendar_scandate -a $line; then
+ lines_unsorted+=("${(l.16..0.)REPLY}:$line")
+ else
+ lines_failed+=($line)
+ fi
+ done
+
+ if (( ${#lines_unsorted} )); then
+ lines_sorted=(${${(o)lines_unsorted}##[0-9]##:})
+ fi
+
+ {
+ for line in "${lines_failed[@]}"; do
+ print "$line # BAD DATE"
+ done
+ (( ${#lines_sorted} )) && print -l "${lines_sorted[@]}"
+ } > $new
+
+ if [[ ! -s $new ]]; then
+ print "Writing to $new failed."
+ return 1
+ elif (( ${#lines_failed} )); then
+ print "Warning: lines with date that couldn't be parsed.
+Output (with unparseable dates marked) left in $new"
+ return 1
+ fi
+
+ if ! mv $calendar $calendar.old; then
+ print "Couldn't back-up $calendar to $calendar.old.
+New calendar left in $new"
+ return 1
+ fi
+ if ! mv $new $calendar; then
+ print "Failed to rename $new to $calendar.
+Old calendar left in $calendar.old"
+ return 1
+ fi
+
+ print "Old calendar left in $calendar.old"
+} always {
+ (( ${#lockfiles} )) && rm -rf $lockfiles
+}