How I use remind(1)
Overview
The
remind
calendar utility offers an amazing degree of power
for doing calendar-related scheduling unmatched by
any other calendar program I've used.
If you haven't encountered it before,
this
presentation (PDF)
gives a good overview of some of the features,
as do
Linux Journal
and
43 Folders.
In this post,
I'll walk through
how I use both the basic
and advanced features.
This post makes a couple assumptions about you as the reader:
- General working knowledge of the CLI
-
While most of the shell commands
should work in the majority of Unix-like shells
(
/bin/sh
,bash
,ksh
,zsh
, orcsh
/tcsh
) some have slight syntax differences. Such differences might include thefor
loop syntax (csh
/tcsh
has a different syntax than thesh
style loops I use), and what variables the shell provides (such as$COLUMNS
thatbash
&ksh
provide, which you might have to define or hard-code before using in other shells). If you build from source into a local~/bin/
directory, you will want to know about modifying your$PATH
as well. You don't need a Ph.D. in using the command-line to do any of these, but I do assume basic directory creation & navigation skills usingmkdir
&cd
, as well as moving/renaming and copying withmv
/cp
and output redirection. I also have a section on usinggit
orrcs
to version your reminders but that section aims to help you integrate whatever version-control software you already use rather than try to teach either as a requirement. - General competency with a text editor
-
Reminder files are pure plain-text.
So you can edit them with your favorite
$EDITOR
, whethernano
,emacs
/mg
,vi
/vim
/neovim
,ed
,cat
, a magnetized needle and a steady hand, or butterflies. But this guide assumes that you can edit text files and save them. -
You have
remind
already installed or can install it -
If you administer your personal system,
you should have
root/
sudo
/doas
access to installremind
from a binary package (available in most repositories) or possibly install a C compiler likegcc
/clang
to install from source. If you do not administer your system, you can either talk to your system administrator and ask them to installremind
(try offering baked goods); or you can install from source, specifying a user-local directory such as$HOME/bin/
(as long as you include that in your$PATH
, you have a C compiler on the system, and your administrator didn't mark your$HOME/
directory's mount-point asnoexec
).
Installing
As a package
Initially released in 1990
via USENET on
comp.sources.misc
,
nearly every package repository
should provide
remind
,
making it as easy to install
as any other package:
pkg install remind
doas pkg_add remind
sudo apt-get install remind
From source
Alternatively, download the source
directly from the
latest tarball
(and hopefully verify it using
gpg
with the
PGP key
which should have the fingerprint
738E:4D95:4052:902C:147D:07B2:685A:5A5E:511D:30E2
)
VER=0.3.03.00
wget https://dianne.skoll.ca/projects/remind/download/remind-${VER}.tar.gz{,.sig}
gpg --verify remind-${VER}.tar.gz.sig xvfz remind-${VER}.tar.gz
tar xvfz remind-${VER}.tar.gz
mv remind-${VER} remind
git
:
git clone https://dianne.skoll.ca/projects/remind/git/Remind.git remind
cd remind
./configure
make
sudo make install
make install
you can move/copy the resulting binaries from
remind/src/{remind,rem2ps}
to a
~/bin/
directory
(in your
$PATH
),
create a link from
remind
to
rem
,
and put the
man
pages from
remind/man/*.1
in your
$MANPATH
.
Getting started
Creating your first
remind
file
By default, invoking
rem
expects data in
~/.reminders
and you can just dump all your reminders in there
(though
remind
also lets you
split reminders up into
individual files).
When invoked as
remind
rather than
rem
you must specify the reminder file directly:
remind path/to/reminders.rem
The most basic reminder
consists of
REM
followed by a date of some sort,
followed by
MSG
and the text of the reminder.
A few things to keep in mind
-
remind
ignores blank lines. and treats lines beginning with a#
as comments -
to continue a reminder on the next line,
end the previous line with a backslash
("
\
") -
If you only include part of the date
remind
assumes the missing parts repeat. - You can specify either the numeric day of the month or the day of the week (in which case it will repeat on that day of the week).
-
For the month and week-day,
remind
does not distinguish between upper/lower-case and thus treats "Jan", "jan", "JaN", "jAn", and "JAN" all as the same. -
Additionally, while
remind
requires the first three characters of months ("Jan") and weekday names ("Wed"), you may also fully spell out the month ("January") or weekday name ("Wednesday") if that makes it easier for you to read. -
If you provide more than one weekday,
remind
will match any of them. -
Alternatively, you can write the date in
YYYY-MM-DD
format -
If you omit all of the values,
omit the
REM
too
For example if you use your
%b
- described above, producing "in N days' time", "tomorrow", and "today"
%l
- replaced with "on YYYY-MM-DD", "tomorrow", and "today"
%c
- replaced with the day-of-the-week of the event such as "on Wednesday", then "tomorrow", and "today"
%2
-
replaced with the
AT
time in 12-hour am/pm time, e.g. "at 2:30pm" - the trigger date of the event
- the trigger time of the event
- the priority of the event
- whether to sort timed events before untimed events
- an individual file for myself
- one for my wife
- one for each kid
- one for each kid's school calendar
- birthdays
- anniversaries
- family events
- household chores
- finances
- church events
- one for each place I volunteer
- US holidays
- events at our local library
- work
- one for the kids (combining both kids' personal calendars and their school calendars)
- one for the whole family (combining my calendar and my wife's calendar with the combined kids' calendar)
-
and of course the master
reminders.rem
that contains everything -
the
CLEAR
inside thePUSH
/POP
block means that my globalOMIT
list of holidays doesn't impact the breakfast dates -
if future events bump the event
I only need to add them to the
OMIT
list at the top -
by having a local
OMIT
list as well (consisting of the non-Saturdays) theAFTER
postpones to the following Saturday rather than pushing it to the next day (Sunday) -
The first three reminders
trigger on the first of each month.
However
remind
will test the month-number ($Um
), dividing it by 3 and getting the remainder ("%
" does division and results in the modulus/remainder). If the remainder is 1, we have January/April/July/October; if the remainder is 2, we have February/May/August/November; if the remainder is 0, we have March/June/September/December. -
The second two reminders test
if the currently-considered date
(
today()
, the same as$U
) matches the real-world date (realtoday()
) and only displays theMSG
portion today. -
To find Friday the 13th,
we can't use
REM Fri 13
because this finds the first Friday on or after the 13th of each month. That might or might not coincide with the 13th. Instead, we find all the 13ths and then use theSATISFY
clause to reject those that don't fall on a Friday (0=Sunday, …, 5=Friday, 6=Saturday) -
To find the 5th Monday
we need to use the same procedure
for finding the other Nth
weekdays of the month
(
Mon 29
) but then check that the month of the resulting date ($Um
) falls in the same month (monnum()
) as the date seven days before (-7
) today's date ($U
, the date currently under consideration). -
In the final pair of entries,
I have a club meeting
every Monday night
from August through May.
However, club takes off during Christmas break
(and some other days I've not included here),
so the
OMIT
takes care of this. However, I don't want notifications on every day during the break telling me not to attend club. I only want notifications on the Mondays during the break. Using theSATISFY
clause lets me limit these so they only fall on Mondays ($Uw == 1
). -
remind
encounters the current number of days out for the next instance of this reminder - it encounters a zero (today)
- the function results cease decreasing monotonically (they must continue decreasing, so "10, 5, 7, 0" will bail at 7 because it went up from 5)
- set the prefix color for the labels
- determine and display the file-name-based prefix
-
switch to whatever color
calcolor
currently contains - display the reminder text
- reset back to a normal color
- The date in YYYY/MM/DD format
-
The
SPECIAL
type (in this caseCOLOR
) -
a comma-separated list of
TAG
s associated with the reminder - the duration in minutes
- the start-time in minutes-from-midnight (510 = 60 minutes * 8 hours)
-
the
$RED
,$GREEN
, and$BLUE
values from theCOLOR
(255, 128, and 0 here) - the body of the message, prepended with the time (if present)
-
a literal
# rem2ps begin
-
a line showing the month currently rendering,
the current year,
the current month,
the number of days in the month,
the week-day
(0=Sunday, 1=Monday, … 6=Saturday)
of the first day of the month,
and a boolean 0/1
based on whether the calendar
should display Monday first
(based on the
"
-m
" option passed toremind
) - the days of the week in the local language
- the next month and its number of days
- the previous month and its number of days
-
the list of reminders like
-s
produces -
a literal
# rem2ps end
$EDITOR
to create this
~/.reminders
file
# this is a comment # these and the next (blank) line are ignored REM Jan 1 2020 MSG The first day of the 2020 REM 2020-1-1 MSG Also the first day of the 2020 REM Jan 1 MSG The first day of the year REM 1 MSG The first day of every month REM Jan MSG Every day in each January REM January MSG Also every day in each January REM Jan 1 MSG Every January 1st REM Jan 2020 MSG Every day in January 2020 REM Wed MSG Every Wednesday REM Wednesday MSG Also every Wednesday REM Wed Jan MSG Every Wednesday in every January REM Wed Jan 2020 MSG Every Wednesday in January 2020 REM Mon Wed Fri MSG Every Monday, Wednesday, and Friday REM Sat Sun Jan 2020 MSG Every weekend in January of 2020 MSG With no REM, this happens every single day REM Jan 1 2020 \ MSG This line has a continuation
remind ~/.reminders 2020-01-01 rem 2020-01-01
remind
defaults to the current date.
I most frequently invoke just
rem
By specifying a day of the week
and a numeric date,
remind
will find the next date-match
and will then scan forward
until it finds the matching day of the week.
REM Mon 1 MSG First Monday of the month
REM Mon 1 Jan MSG First Monday in January
REM Mon 1 Jan 2020 MSG First Monday in January, 2020
REM Mon 8 MSG Second Monday of the month
REM Mon 15 MSG Third Monday of the month
REM Mon 22 MSG Fourth Monday of the month
REM Mon 29 MSG Infrequent Fifth Monday of the month
REM Sat Sun 15 MSG Third Saturday or Sunday of the month
SATISFY
clause later,
we can limit this
so it only notifies
on the fifth day
in the
same
month.
Also note that this can have interesting side-effects such as trying to find Labor Day.
Similarly, our city offers a
program for recycling items
that we shouldn't put in our recycle bin
(hazardous household waste,
electronics, etc).
These events occur quarterly
on the Saturday
after the first Friday
of the month.
We can express the date as
REM Sat 2 MSG Recycling center drop-off day
SATISFY
(discussed later)
to ensure it only falls on one of the quarterly dates:
REM Sat 2 SATISFY [$Tm % 3 == 2] \
MSG Recycling center drop-off day
Adding time information
To indicate the time and length of an event use
AT
(in military/24-hr time format;
I wish the parser allowed
for 12-hr AM/PM notation, but alas)
and optional
DURATION
(in hours and minutes).
Here I have a doctor's appointment at 2:30pm
but don't know how long it will run.
And I go to the gym
on Tuesday/Thursday
at 5:00am
for an hour and fifteen minutes:
REM Jan 30 2020 AT 14:30 MSG Doctor's appointment
REM Tue Thu AT 5:00 DURATION 1:15 MSG Gym
MSG
.
Getting advance notice
Sometimes you want a bit of advanced warning
like knowing about your doctor's appointment tomorrow.
Perhaps you want a week of notice
before your mother's birthday
so you can get something in the mail.
Or you want a 60 days of notice
before you have to pay taxes
so you can get all your paperwork in order.
remind
uses
+n
and
++n
to give you
n
days of advanced warning.
The single
+
skips over
OMIT
dates,
while
++
ignores
OMIT
dates.
For now, use the
++n
form for simplicity.
REM Jan 1 2020 ++1 AT 14:00 MSG Doctor's appointment
REM May 12 ++7 MSG Mom's birthday
REM Apr 15 ++60 MSG Taxes
remind
recognizes that you likely want this advanced notice
in the agenda view
but smartly prevents the advanced noticed from cluttering up
in the ASCII
week or month calendar views
(unless you explicitly request them with
-pa
or
-sa
).
While less useful to get the same reminder several days out,
remind
lets you
adjust the reminder text
based on the number of days out
to make these more helpful.
Adjusting dates
To adjust a date backwards, use
-n
or
--n
to specify the number of days.
At first glance,
this provides little benefit
over just doing the math
and hard-coding it
for normal reminders:
REM 15 --3 MSG Really the 12th
REM 12 MSG Just use the 12th instead
REM 1 --1 MSG Last day of the month
REM 1 --7 MSG One week left in the month
REM 1 Wed --7 MSG Last Wednesday of the month
REM Feb 1 Wed --7 MSG Last Wednesday in January
As with
+n
vs.
++n
,
the single
"-
"
jumps over
OMIT
dates,
(so they can end up going back
more than n days
if they encounter an intervening holiday);
while the double
"--
"
ignores
OMIT
dates.
These adjustments can combine
REM 1 --1 ++3 MSG Last day of the month
Repeating reminders
As shown above,
if you omit portions of a
REM
statement,
remind
treats the missing date part
as a repeat.
This lets you create reminders
such as
birthdays and anniversaries,
holidays,
weekly events like church,
or invoicing customers
on a given day of each month:
REM Jan 13 MSG Steve's birthday
REM Feb 18 ++7 MSG Mom & Dad's anniversary
REM Apr 1 ++1 MSG April Fool's Day
REM Sun AT 9:00 MSG Church
REM 15 MSG Invoice customers
On occasion I need a reminder every N days
like picking up a 90-day prescription
that started on January 27th of 2019.
To do this,
use an asterisk followed by the number of days to repeat.
REM Jan 27 2019 *90 MSG Pick up medication
For repeating events,
remind
lets you specify date ranges
using
FROM
,
UNTIL
,
and
THROUGH
keywords.
For example, if your vacation starts on December 21st
and repeats daily
(*1
)
until January 1st
you can use:
REM Dec 21 2019 *1 UNTIL Jan 1 2020 MSG Vacation
THROUGH
keyword simplifies the common
"*1
"
use case,
so this works exactly the same:
REM Dec 21 2019 THROUGH Jan 1 2020 MSG Vacation
*2
)
starting on January 1st
through the end of the month:
REM Jan 1 2019 *2 UNTIL Jan 31 2020 MSG Take medication
REM Wed FROM Aug 21 2019 UNTIL Jun 1 2020 MSG Game Night
REM 12 FROM Aug 21 2019 UNTIL Jun 1 2020 MSG School year meeting
Modifying the output
Other views
If you have no reminders for a given day,
remind
prints "No reminders."
Otherwise,
remind
prints a banner
of the form
"Reminders for Wednesday, 1st January, 2020 (today)"
followed by an agenda-view of the reminders for that day.
If you want more than one day worth,
specify the number of days with
"*n
".
Note that you should quote the "*"
either by escaping it with a backslash
or by wrapping it in quotes.
Otherwise the shell might expand it
to a filename in your current directory.
To show four days of reminders:
rem \*4
rem '*4'
remind
also offers an ASCII calendar view
for either the month or week:
rem -c # a month view
rem -c2 # two month view
rem -c 2019-8-1 # one-month-view of 2019-08-01
rem -c+ # a week view
rem -c+2 # two week view
-w$COLUMNS
rem -c+2 -w$COLUMNS
remind
automatically detects the screen width
if you output to a
TTY
so you no longer need to specify this explicitly
unless you want something other than the current screen width.
Other message-types
Up to this point,
we've only discussed the
MSG
message-type.
remind
also provides
CAL
for events that should only show up
in the
week/month view
but not the agenda view, and
MSF
which behaves the same as the
MSG
except it wraps long text
in the agenda view.
REM Jan 1 2020 CAL This only appears on the calendar view
REM Jan 1 2020 MSF This is a very long reminder \
and includes several lines \
of text all with the escaping backslash \
but when you view it \
in the agenda view \
it will be wrapped \
so that it formats nicely.
remind
provides a couple other reminder-types:
RUN
for running an external command,
SPECIAL
,
for conveying out-of-band information,
and
PS
/PSFILE
for passing PostScript information
to a PostScript-processing back-end
which I'll skip for now).
Getting rid of blank lines
I find it annoying
to get a blank line after each reminder.
You can prevent this
by ending reminder text with a
%
REM Jan 30 2020 +3 MSG Doctor's appointment%
%
from all of the other examples in this post,
but I put it on
every single reminder.
Selectively showing text
You might want
certain text to appear
on the daily agenda calendar
but you don't need that level of detail
on the
week/month calendar view.
You can wrap the text you want
on the week/month calendar view with
%"
…%"
REM Jan 30 2020 +3 MSF %"Doctor%"'s appointment \
123 Main St., Anytown, NY\
800-555-1212
%"
markers)
on the agenda view
but only show
"Doctor"
on the visual calendars.
Substitution filter
Your reminder-message can include escape sequences
(beginning with a
%
)
that adjust based on the context.
So instead of a reminder that just reads
"Mom's birthday"
you can use
REM May 12 ++7 MSG Mom's birthday %b
remind
will display
"Mom's birthday
in 7 days' time",
…,
"Mom's birthday
in 2 days' time",
"Mom's birthday
tomorrow",
and finally
"Mom's birthday
today".
The
man
page for
remind
has a whole collection of these
under the
"SUBSTITUTION FILTER"
section.
I most frequently use
REM Jan 1 2020 +1 AT 14:30 MSG Doctor appointment %b %2
Tweaking the banner
remind
prints a "banner"
before each day's agenda.
The default
BANNER
contains
"Reminders for %w, %d%s %m, %y%o:
"
but you can use any of the
substitution filter
formatting sequences that fit your wants.
The extra blank line after the
BANNER
bothers me so let's get rid of that
by putting a
%
at the end:
BANNER Reminders for %w, %d%s %m, %y%o:%
BANNER Reminders for %w, %m %d%s, %y%o:%
When I use
rem '*3'
to
get 3 days worth of reminders
I dislike how it formats the dividers
because they all flow together visually,
making it hard for me to read.
Fortunately,
remind
lets you use an
IF
/ELSE
/ENDIF
block to conditionally
tweak this as well:
IF defined("subsequent_iteration")
BANNER ---------------------%_%w, %m %d%s, %y%o:%
ELSE
BANNER %w, %m %d%s, %y%o:%
SET subsequent_iteration 1
PRESERVE subsequent_iteration
ENDIF
subsequent_iteration
is not defined,
so we set the default
BANNER
and then set/define
subsequent_iteration
.
The
PRESERVE
keyword then keeps the value of
subsequent_iteration
defined through subsequent iterations.
Now defined,
remind
takes the
ELSE
block,
prefixing the banner with a row of dashes,
a new-line,
and then the normal banner text.
Sorting
By default,
remind
prints agenda items in the order in which
they appear in the reminder files.
Using the
-g
flag tells
remind
to sort the output.
Following the
-g
comes up to four
"a" or "d"
characters to indicate
ascending
or
descending
order.
Positionally, these indicate
rem -gaad
sorts first by trigger date (ascending),
then by trigger time (ascending),
and finally by priority (descending),
while not specifying whether
timed or untimed events should come first.
Sub-BANNERs for sorting
When sorting reminders,
I like to have a visual indication
between where today's reminders stop
and subsequent days'
advanced notices
so I override the special
sortbanner()
function.
Much like the
BANNER
,
this lets you define
a separator that comes
between these events.
I use the
$SortByDate
,
$SortByPrio
,
and
$SortByTime
variables to determine if any
sort-order
creates these logical breaks,
and then use a
substitution filter
in my function-result:
SET banner_str "…"
IF $SortByDate + $SortByPrio + $SortByTime > 0
BANNER %
FSET sortbanner(x) iif(x == $U, \
banner_str, \
"----%B----%" \
)
ELSE
BANNER [banner_str]
ENDIF
BANNER %
to suppress the regular banner,
letting the
sortbanner()
assume those duties.
Also, by capitalizing the substitution-filter
(%B
instead of %b
)
it capitalizes the resulting output.
Next instance of reminders
If you want to know the next time
a reminder will occur,
remind
offers the "next"
(-n
)
flag.
Beware as this produces ~900 reminders for me
(one for each of my 1600+
reminders yet to happen in the future),
but you can
grep
this output or pipe it to
$PAGER
rem -n |
grep Alice |
sort |
less
File management
Organizing reminders
While
remind
will happily let you dump all your reminders in
~/.reminders
,
it also provides functionality
to split and combine files using the
INCLUDE
directive.
I like to organize reminders
into files based on reminder-type,
so I created a directory to house them all
cd
# don't tromp over existing reminders if we have them
mv .reminders original_reminders.rem
mkdir -p ~/.config/remind
echo 'SET remind_dir getenv("HOME") + "/.config/remind/"' > \
~/.config/remind/reminders.rem
~/.reminders
to point to my main reminder file:
ln -s .config/remind/reminders.rem ~/.reminders
cd ~/.config/remind
remind
magic
(see the section on
expressions
below)
to dynamically find your reminder directory
for later use in the
INCLUDE
statements.
This lets me move
or
rsync
my reminders to another machine
under a different
$HOME
and everything will continue to work.
Alternatively,
you can create a link
to the containing folder
instead of a reminder file
full of
INCLUDE
lines:
ln -s .config/remind/ ~/.reminders
remind
will process all of the
*.rem
files in the target folder
in glob order.
If you want a particular order,
you can name the files
with a prefix like
010-birthdays.rem
,
015-anniversaries.rem
,
020-work.rem
,
etc.
Initial file types
I start by creating a
helpers.rem
file to store a variety of
helper functions and constants.
touch ~/.config/remind/helpers.rem
INCLUDE
them into the master
reminders.rem
file:
for f in ushol birthdays anniversaries work bills
do
echo "INCLUDE [filedir()]/helpers.rem" > ~/.config/remind/${f}.rem
echo "INCLUDE [remind_dir]/${f}.rem" >> ~/.config/remind/reminders.rem
done
man
-page-per-day and
a schedule of when the Cowboys play
(I don't care much about football,
but Dunkin offers a free medium coffee
on the day following a Cowboys win
so my calendar consists of reminders for the day
following
a game to check the score).
I also have a couple calendars
consisting only of
INCLUDE
statements to combine other calendars:
At first glance,
splitting out each file doesn't seem to offer any advantages.
However, this lets you do things like
show a calendar of birthdays only
without all the other reminders,
or show just a school calendar.
remind -c ~/.config/remind/birthdays.rem
The majority of my
ushol.rem
file comes from the
examples on the
remind
website.
It creates a bunch of
OMIT
entries useful
for any reminders
that get bumped because of US holidays.
Advanced features
While all that is useful,
other calendar programs like
Google Calendar,
Microsoft Outlook,
or even the venerable
calendar(1)
CLI utility
can do many of those things.
However
remind
really shines when you want to do things
that these others simply can't.
Expression evaluation
For even more power & flexibility,
remind
lets you
define variables & functions
as well as
evaluate expressions
(documented in the man-pages as
"EXPRESSION PASTING").
To set a variable, use
SET
and
remind
will evaluate the right-hand side
(the
"4 - 3
"
in the example),
assigning the result
to the variable on the left-hand side
(the
myvar
in the example):
SET myvar 4 - 3
FSET
.
The example below creates a function named
add_three
that takes one parameter named
x
.
When called,
it adds three to the number passed to it
and returns the result:
FSET add_three(x) 3 + x
REM Jan 1 2020 +1 MSG 4 - 3 = [4 - 3]
REM Jan 1 2020 +1 MSG 4 - 3 = [myvar]
REM Jan 1 2020 +1 MSG 4 - 3 = [add_three(-2)]
REM Jan [4 - 3] 2020 +1 MSG 4 - 3 = 1
REM Jan 1 [add_three(2017)] +1 MSG 4 - 3 = 1
REM Jan 1 2020 +[myvar] MSG 4 - 3 = 1
SET VacationStart date(2020, 7, 15)
SET VacationEnd VacationStart + 10
REM [VacationStart - 60] OMIT Sat Sun BEFORE \
MSG Submit time-off paperwork
REM [VacationStart - 50] MSG Buy tickets
REM [VacationStart - 3] OMIT Sun BEFORE MSG Stop mail delivery
REM [VacationStart - 1] MSG Pack bags
REM [VacationStart] THROUGH [VacationEnd] MSG Vacation
REM [VacationEnd + 1] OMIT Sat Sun AFTER MSG Back to work
OMIT
functionality).
Using functions in reminder text
If I know the birthday or anniversary of a friend,
I like to include that in the reminder text.
So in my
helpers.rem
I have
FSET born(y) "(" + ($Uy-y) + "yo)"
FSET married(y) (ord($Uy-y)) + " anniversary"
REM Apr 1 +7 MSG %"Bob & Alice [married(1999)]%" %b
REM Jan 14 +7 MSG %"Bob [born(1980)]%" %b
Bob & Alice 20th anniversary tomorrow
Bob (40yo) in 3 days' time
Days until an annual event
As an example of function use,
say you want to determine the number of days
until an annual event.
If it has already passed this year,
you want to refer to the event next year;
if it hasn't already passed this year,
you want to refer to the event this year.
A function wraps up this functionality nicely:
FSET DaysUntil(dt, m, d) iif(\
date(year(dt), m, d) > dt,\
date(year(dt)+1, m, d),\
date(year(dt), m, d)\
) - dt
FSET DaysTillStr(dt, m, d) \
DaysUntil(dt, m, d) + \
plural(DaysUntil(dt, m, d), " day")
FSET ChristmasDays(dt) DaysTillStr(dt, 12, 25)
MSG %"[ChristmasDays($T)] until Christmas%"%
When run, this will produce output
rem 2020-12-1
Reminders for Tuesday, 1st December, 2020:
24 days until Christmas
rem 2020-12-28
Reminders for Monday, 28th December, 2020:
362 days until Christmas
Specifying the priority of a reminder
While I don't currently use task priority much,
remind
allows you to provide a
PRIORITY
modifier (an integer between 0 and 9999)
for an event.
If unspecified,
remind
sets the priority to
$DefaultPrio
(5000 by default).
As far as I can tell,
there's no inherent meaning to the priority,
so if it makes more sense to you that
9999 means "high priority" and
0 means "low priority",
then use them as such;
if it makes more sense to you that
0 means "high priority" and
9999 means "low priority",
use that.
I tend to declare a couple constants using
SET
and then use those,
making it easier to reverse the meaning
should I need to.
SET PRIORITY_HIGH 9999
SET PRIORITY_LOW 0
REM Jan 1 2020 \
AT 10:00 DURATION 1:00 \
PRIORITY [PRIORITY_HIGH] \
MSG Very important meeting
REM Jan 1 2020 \
AT 14:00 DURATION 1:00 \
PRIORITY [PRIORITY_LOW] \
MSG Unimportant thing
Adding a prefix/suffix to agenda items
remind
provides a pair of special functions,
msgprefix(p)
and
msgsuffix(p)
,
that emit a prefix & suffix
around each agenda item.
These functions take an explicit
priority
parameter
(although you can reference other variables and functions)
allowing you to do things like
# Annotate high-vs-low priority
FSET is_low_prio(p) p < $DefaultPrio
FSET is_high_prio(p) p > $DefaultPrio
FSET msgprefix(p) \
iif(is_high_prio(p), "+ ", \
iif(is_low_prio(p), "- ", \
" "))
# add some asterisks after high-priority tasks
FSET msgsuffix(p) \
iif(is_high_prio(p), " *****", "")
is_low_prio
and
is_high_prio
functions accordingly)
or, using some of the
tricks from the section on color
FSET msgprefix(p) \
iif(is_high_prio(p), Red, \
iif(is_low_prio(p), Gry, \
Nrm))
FSET msgsuffix(p) Nrm
Omitting and shifting dates
Some events shift around
or get cancelled
based on whether they fall
on particular days like holidays,
weekends,
or even just for arbitrary reasons
("somebody else booked the conference room
so we need to move it to next week").
The
OMIT
keyword tells
remind
how to identify these cases.
Omitting/shifting for a single event
You might have a project at work
that runs for a couple weeks
but you don't want your agenda
to display it on weekends
because you have work/life boundaries.
Use the
OMIT
keyword to tell which days, and
SKIP
to tell
remind
to ignore events that fall on these days.
Put them in your
REM
command:
REM Jan 2 2020 THROUGH Jan 15 2020 \
OMIT Sat Sun SKIP \
MSG Work project
AFTER
the weekend
to the following Monday:
REM 15 OMIT Sat Sun AFTER MSG Payday
BEFORE
the weekend
so that payment arrives on time:
REM 15 OMIT Sat Sun BEFORE MSG Pay water bill
Omitting/shifting for multiple events
Certain events such as federal holidays
can displace multiple reminders.
Rather than try and
OMIT
every single holiday
in every single reminder,
remind
provides bare
OMIT
entries similar to
REM
but they get added to a
contextual
omit-list.
If you have important weekly meetings
on Monday and Friday
and an unimportant meeting on Wednesday,
the following will bump the important Monday meeting
back to Tuesday if Christmas fell on Monday,
will bump the important Friday meeting back to Thursday
if Christmas fell on a Friday,
and cancel the unimportant Wednesday meeting
if Christmas fell on a Wednesday,
OMIT Jul 4 MSG 4th of July
OMIT Dec 25 MSG Christmas
REM Mon AFTER MSG Important Monday Meeting
REM Wed SKIP MSG Unimportant Wednesday meeting
REM Fri BEFORE MSG Important Friday Meeting
rem 2015-12-24 '*2' # got moved to 24th
rem 2017-12-25 '*2' # got moved to 26th
rem 2019-12-24 '*3' # got cancelled
OMIT
entries.
Omit notice and adjusting backwards
As discussed in the
advanced-notice
and
adjusting-dates
sections,
remind
can give you advanced notice of reminders
and adjust dates backwards a certain number of days.
In that section,
I recommended using the double
++
/--
because they ignore
OMIT
dates, making them easier to understand.
However sometimes you want extra days' notice
if intervening
OMIT
holidays happens
(such as closing the bank or post-office)
in which case
you should use the single
+
form.
Likewise, use
-n
to find the last day of the month,
but you want it to skip over
OMIT
dates.
OMIT Dec 25 MSG Christmas
REM Dec 26 +1 MSG shows on the 24th, 25th, and 26th
REM Dec 26 ++1 MSG shows on the 25th and 26th but not 24th
REM Jan 1 -7 MSG shows on the 24th
REM Jan 1 --7 MSG shows on the 25th
Omit contexts
Because not all places or calendars
use the same list of holidays
to determine
OMIT
days,
remind
lets you create custom
OMIT
contexts with
PUSH
(ending those temporary contexts with
POP
)
and optionally use
CLEAR
to temporarily remove previous
OMIT
dates.
As an example,
our kids' schools close for (most) federal holidays
but each has its own list of other days off
such as in-service days
and bad-weather make-up days.
# our son's school has certain days off
PUSH
OMIT Apr 27 2020 MSG %"In service day%" \
No school for son
OMIT May 22 2020 MSG %"Bad-weather make-up day%" \
No school for son
REM Mon Tue Wed Thu Fri \
FROM Aug 14 2019 \
UNTIL Jun 1 2020 \
SKIP \
MSG %"Son's school%"
POP
# our daughter's school has other days off
PUSH
OMIT Apr 13 2020 MSG %"In service day%" \
No school for daughter
OMIT May 8 2020 MSG %"Bad-weather make-up day%" \
No school for daughter
REM Mon Tue Wed Thu Fri \
FROM Aug 14 2019 \
UNTIL Jun 1 2020 \
SKIP \
MSG %"Daughter's school%"
POP
OMIT
lists instead.
For example,
a group of guys get together for breakfast
on the first Saturday of each month.
But a couple times
someone else has booked our venue,
pushing it back a week.
PUSH
CLEAR
OMIT 5 Oct 2019 MSG %"No breakfast get-together%" \
(venue unavailable)
REM Sat 1 +1 AT 07:45 DURATION 1:30 \
OMIT Sun Mon Tue Wed Thur Fri AFTER \
FROM Aug 31 2019 \
MSG %"Breakfast get-together%" %b %2
POP
Frankly, as a guideline,
I lean towards putting
every
OMIT
inside its own
PUSH
/CLEAR
/POP
block.
Omit function
On occasion you might find it easier
to describe the days you want to
OMIT
with a function.
remind
lets you create a such a function
that takes a date as the only argument,
and returns 0
if it should consider the date,
or returns non-0 if
it should treat the date as omitted.
Use the
OMITFUNC
modifier to tell
remind
to ignore the local
and global
OMIT
s
and use the
OMITFUNC
instead.
FSET myomit(d) …
REM OMITFUNC myomit MSG Some event
OMIT
list into consideration
or omit certain days of the week
like you might with a local
OMIT
,
use the
isomitted()
function in your
OMITFUNC
FSET near_a_new_moon(d) \
moonphase(d) < 20 || \
moonphase(d) > 340
FSET allmyomits(d) \
wkdaynum(d) == 0 || \
wkdaynum(d) == 6 || \
isomitted(d) || \
near_a_new_moon(d)
REM OMITFUNC allmyomits AFTER \
MSF This doesn't happen \
on weekends, \
OMIT dates, \
or around a new moon.
moonphase()
function takes a date
and returns a number from 0 to 359
with 0="new moon"
and 180="full moon",
so the
near_a_new_moon
function returns true
within ~20° on either side
of a new moon)
Using
OMIT
for more complex dates
In certain cases
you might want to create an
OMIT
that falls on certain days
that otherwise work with regular reminders.
However if you try to do something like
OMIT Mon Feb 15 MSG President's Day
remind
considers the "Mon" part of the
OMIT
a syntax error.
To get around this,
create it as a regular reminder first,
then use the resulting
trigdate()
to create the
OMIT
:
REM Mon Feb 15 MSG President's Day
OMIT [trigdate()]
Using
SATISFY
to conditionally test events
Sometimes you want
remind
to perform additional tests on a date
to determine whether an event should occur,
and if not,
continue seeking the next matching date
and test that.
The
SATISFY
clause lets you
evaluate an expression
to do just that.
You might only want something to happen every Nth month
or test that something only occurs on certain days.
As previously suggested
when discussing the Nth weekday of a month
you might need to check that the date
falls in the same month.
REM 1 SATISFY [$Tm % 3 == 1] \
MSG The first of every 3rd month starting January
REM 1 SATISFY [$Tm % 3 == 2] \
MSG The first of every 3rd month starting February
REM 1 SATISFY [$Tm % 3 == 0] \
MSG The first of every 3rd month starting March
REM MAYBE-UNCOMPUTABLE \
SATISFY [$U == realtoday()] \
MSG Only appears today
REM [realtoday()] \
MSG Also only appears today
REM 13 SATISFY [$Uw == 5] MSG Friday the 13th
REM Mon 29 SATISFY [monnum($T - 7) = $Tm] \
MSG Infrequent Fifth Monday of the month
PUSH
CLEAR
OMIT Dec 23 2019 THROUGH Jan 6 2020 \
SATISFY [$Uw == 1] \
MSG %"No club%"
REM Mon \
FROM Aug 1 2019 \
UNTIL May 31 2020 \
OMIT SKIP \
MSG Club
POP
Fine-grained scheduling
Sometimes you want
advanced warning
of an event at certain intervals like
++n
provides,
but you don't want those warnings
on every day during that interval.
The
WARN
modifier lets you provide a function
that tells
remind
which days you want reminders.
The function gets called repeatedly
first with 1,
then with 2,
etc.
The search stops if one of the following conditions occur:
choose()
function does this,
so to get reminders
30 days out,
10 days out,
5 days out,
and 2 days out:
FSET mywarnfunc(i) choose(i, 30, 10, 5, 2, 0) REM Jan 1 2020 WARN mywarnfunc \ MSG Get car inspected
remind
should produce a reminder for today,
it starts by calling this
mywarnfunc
function with a parameter of 1.
The
choose()
function returns 30,
so if January 1st, 2020
is 30 days from the current date in consideration,
it will remind.
If not,
remind
calls
mywarnfunc
with a parameter of 2.
This time, the
choose()
returns 10.
Again if January 1st, 2020
is 10 days from the current date,
you will get the reminder.
Same with 5 and 2 days out.
Finally, if none of the results
match,
remind
tests if the date in question
is January 1st, 2020.
If you want to skip
OMIT
days, return a negative number.
The check for monotonically decreasing numbers
only tests the absolute value so a function like
FSET mywarnfunc(i) choose(i, 30, -10, 5, -2, 0) REM Jan 1 2020 \ WARN mywarnfunc \ MSG Get car inspected
OMIT
days),
5 days out,
and 2 days out
(also skipping over
OMIT
days).
remind
provides a similar
SCHED
functionality for fine-grained
lead time for
AT
-style
reminders,
notifying at a series of times:
FSET mynotice(i) choose(i, 4*60, 2*60, 30, 0) REM January 1 2020 AT 10:00 \ SCHED mynotice \ MSG Doctor
man
-pages.
Miscellaneous tricks
As I've built up my
remind
files, I've developed a collection
of tricks and tips
that I use for various scenarios.
Labels
Because I have
a file for each type of reminder,
I find it handy to prefix each item with its filename
using the special
msgprefix(x)
function
(which I also
use for colorizing agenda reminders):
# fileprefix() reduces the current
# path+filename+extension
# to just the filename
# /path/to/file.rem -> "FILE"
FSET fileprefix() upper( \
substr( \
filename(), \
strlen(remind_dir)+1, \
strlen(filename())-4 \
) \
)
FSET msgprefix(x) fileprefix() + ": "
Friday, 27th December, 2019 (today):
USHOL: Chanukah 5
BIRTHDAYS: David (10yo) in 3 days' time
BIRTHDAYS: Annabelle (12yo)
BIRTHDAYS: Nate today
MANPAGES: man 1 ed
SCHOOL: Christmas: No School
FINANCES: Pay gas bill (today)
HOUSEHOLD: Garbage day (Delayed) today
msgprefix(x)
function:
SET topic ""
FSET msgprefix(x) iif(strlen(topic) > 1, topic + ": ", "")
FSET born(y) "(" + ($Uy-y) + "yo)"
FSET married(y) (ord($Uy-y)) + " anniversary"
SET topic "Birthdays"
REM Jan 1 MSG %"Mom%" [born(1950)]
REM Jan 6 MSG %"Dianne%" [born(1970)]
SET topic "Anniv"
REM Apr 1 +7 MSG %"Bob & Alice [married(1999)]%" %b
SET topic "Work"
REM Fri AT 7:30 MSG %"Meeting%" %2
Color
As a rule,
I generally want plain-text reminders,
whether emailing them to myself
or displaying a calendar with
rem -c
.
But when at the command-line,
I like a bit of color.
General per-reminder foreground color
As one of the
SPECIAL
codes,
remind
offers
COLOR
(or it will happily accept
"COLOUR
")
for reminders
that alters the color used
when generating a
week/month calendar with
-cc+
or
-cc
.
The entry takes three numbers as arguments,
the
Red,
Green, and Blue values,
each ranging from 0 to 255,
followed directly by the
MSG
-style text
(without the
MSG
keyword):
REM Jan 1 SPECIAL COLOR 255 0 255 \
Happy New Year in bright magenta
This makes it easy to colorize
a single reminder,
but it only works in week/month view
(or external apps that support it
via the
-p
option),
not the agenda view
(unless you have version 3.3.0 or later
where the
-@
option enables color in the agenda view).
However sometimes I want to colorize
whole swaths of reminders
and do it on the agenda view.
I want all my birthday
and anniversary reminders
in green,
my personal reminders in blue,
my wife's reminders in bright magenta,
the kids' reminders in cyan,
church/volunteering reminders in magenta,
work in yellow,
and some low-priority entries in gray.
I don't want to have to specify
the color for each and every one
of my 1,600+ reminders.
Prior to the addition of
$DefaultColor
in version 3.3.0
To do this,
I first add a number of
ANSI escape codes
as constants to my
helpers.rem
that I can then use elsewhere.
# ANSI colors SET Esc CHAR(27) SET Nrm Esc + "[0m" SET Blk Esc + "[0;30m" SET Red Esc + "[0;31m" SET Grn Esc + "[0;32m" SET Ylw Esc + "[0;33m" SET Blu Esc + "[0;34m" SET Mag Esc + "[0;35m" SET Cyn Esc + "[0;36m" SET Wht Esc + "[0;37m" SET Gry Esc + "[30;1m" SET BrRed Esc + "[31;1m" SET BrGrn Esc + "[32;1m" SET BrYlw Esc + "[33;1m" SET BrBlu Esc + "[34;1m" SET BrMag Esc + "[35;1m" SET BrCyn Esc + "[36;1m" SET BrWht Esc + "[37;1m"
Then, in my base
reminders.rem
file,
I have the following:
IF defined("COLOR")
SET bannercolor BrWht
SET calcolor Nrm
SET labelcolor Nrm
# if desired, use the priority "x"
# to change the prefix color
FSET prefixcolor(x) Nrm
FSET color() calcolor
FSET msgsuffix(x) Nrm
ELSE
SET bannercolor ""
SET calcolor ""
SET labelcolor ""
FSET prefixcolor(x) ""
FSET color() ""
ENDIF
FSET msgprefix(x) \
prefixcolor(x) + \
fileprefix(x) + \
": " + \
color()
IF defined("subsequent_iteration")
BANNER [labelcolor]=====================[bannercolor]%_%w, %d%s %m, %y%o:%
ELSE
BANNER [bannercolor]%w, %d%s %m, %y%o:%
SET subsequent_iteration 1
PRESERVE subsequent_iteration
ENDIF
COLOR
on the command-line
and sets up functions
& constants
for default colors.
I then set a color at the top of each of my files
after including the
helpers.rem
and it changes the color of everything following,
allowing me to have multiple color blocks
in the same file
INCLUDE [filedir()]/helpers.rem
SET calcolor Ylw
REM 1 MSG %"Send invoice%"
INCLUDE [filedir()]/helpers.rem
SET calcolor BrBlu
REM Jan 10 MSG %"Alice%"
REM Jan 14 +7 MSG %"Bob [born(1980)]%" %b
SET calcolor Blu
REM Jan 6 MSG %"Dianne's birthday%"
msgprefix()
and
msgsuffix()
functions have special meaning to
remind
allowing the above to
I can then use
rem -iCOLOR=1
to get my results in color.
(ignore the fact that the events actually fall on different dates)
BIRTHDAYS: Alice
BIRTHDAYS: Bob (39yo)
BIRTHDAYS: Dianne
WORK: Send invoice
calprefix()
and
calsuffix()
functions doesn't work either)
After the addition of
$DefaultColor
in version 3.3.0
I submitted a patch that has
(with modifications)
made it into the mainline codebase
in version 3.3.0,
released .
This patch allows you to set a
$DefaultColor
variable combining the benefits of the
COLOR
keyword
(appearing on the week/month calendars
and getting passed to external utilities)
with the benefits of my
calcolor
/msgprefix()
/msgsuffix()
trick above
(colorizing the agenda view,
without needing to specify the color
for every single event).
SET $DefaultColor "0 255 0"
# birthdays for which I take some sort of action
REM Jan 10 MSG %"Alice%"
REM Jan 14 +7 MSG %"Bob [born(1980)]%" %b
SET $DefaultColor "0 128 0"
# birthdays I want to know about
REM Jan 1 MSG %"Eric's Mom's birthday%"
SET $DefaultColor "-1 -1 -1"
# Other misc. birthdays in the original default color
REM Jan 6 MSG %"Dianne's birthday%"
remind
with the
-@
parameter,
it will colorize agenda items for you
in addition to the week/month calendar views.
Raw output
While pretty output works well for humans,
sometimes I desire to pipe output to another program.
I have no interest in trying to parse the
week or month calendar views.
The agenda view can serve
as a primitive & fragile output format
for post-processing.
However,
remind
provides the
-p
and
-s
options to produce machine-readable output.
This output format works much better
with tools such as
grep
,
sed
,
awk
,
and
python
/perl
/ruby
,
as well as the
tkremind
program.
remind
also allows your reminders
to convey additional metadata
to these back-ends.
Given this reminder:
REM 2020-01-29 \
AT 8:30 DURATION 3:15 \
TAG work \
TAG home \
SPECIAL COLOR 255 128 0 \
my message
rem -s 2020-01-29
2020/01/29 COLOR work,home 195 510 255 128 0 8:30-11:45am my message
rem -p 2020-01-29
# rem2ps begin
January 2020 31 3 0
Sunday Monday Tuesday Wednesday Thursday Friday Saturday
December 31
February 29
2020/01/29 COLOR work,home 195 510 255 128 0 8:30-11:45am my message
# rem2ps end
-s
offers a simplified format
of only the calendar data:
-p
option wraps it in the following meta-data:
# rem2ps end
indicates the end of one month
but the receiving-program
should handle the case
that more months' data could follow.
The date
(first column)
should always appear,
but the other fields might display an asterisk
("*
")
if not present in the reminder.
SPECIAL
reminders
While I don't give much space to them,
remind
's
SPECIAL
token lets you pass out-of-band information
to external back-ends.
This can include things like
changing the color of an entry
(with
SPECIAL COLOR $RED $GREEN $BLUE
),
shading the background of a calendar square
(with
SPECIAL SHADE $GRAYLEVEL
or
SPECIAL SHADE $RED $GREEN $BLUE
),
drawing the phase-of-the-moon
(with
SPECIAL MOON $PHASE
),
or adding
per-week
annotations
(with
SPECIAL WEEK $TEXT
).
I use
rem2ps
in the example regarding
relative dates
but don't employ any of the
SPECIAL
commands therein.
RUN
reminders
The
RUN
message-type acts much like a
MSG
message-type except that
after passing the body through the
substitution filter
it executes the resulting command
instead of displaying the result.
Beware of quoting and escaping issues.
While you can do things like
REM Jan 31 +3 RUN echo Hello %b,
echo Hello in 2 days' time
REM 1 ONCE RUN backup.sh [$U]
backup.sh 2020-01-01
ONCE
keyword will only run the reminder once,
even if you invoke
remind
more than once in a given day.
However, it relies on the
atime
(most recent access-time)
of the reminder file.
Since my
/etc/fstab
sets
noatime
on all of my mount-points,
any reminders marked with
ONCE
run every time.
I keep a list of "to-do" items
so I tried including those
in my reminders:
REM SATISFY [$U == realtoday()] RUN head -5 ~/todo.txt
I also experimented
with keeping larger stretches of prose
in an external file like
BEGIN: gifts
END: gifts
BEGIN: FakeCON
Flight:
Southwest Flight 3141 DAL → BWI
Departs: 1:41pm
Arrives: 3:14pm
Confirmation number: 1414
Car:
Thrifty reservation# 15278-3955
Hotel:
Marriott
753 Old Oak
Reservation: 20200115732
Member#: 1783613807
END: FakeCON
BEGIN: other
END: other
RUN
to emit the content
FSET external_annotation(marker) \
"sed -n '/^BEGIN: *" + marker + "$/,/^END: *" + marker + "$/p' " + \
filedir() + "/annotations.txt"
REM Feb 18 2020 RUN [external_annotation("FakeCON")]
RUN
in my day-to-day.
Tagging
Similar to the
SPECIAL
keyword, the
TAG
keyword lets you assign one or more tags
to your reminder.
REM 15 TAG work MSG Run commission report
REM 1 TAG home TAG chores MSG Test smoke alarms
REM 1 TAG car TAG chores MSG Check oil levels
remind
doesn't do anything with them directly,
it passes them through
to other back-ends
when using the
raw-output
modes.
Because we
write monthly letters
to our grandparents
I use this functionality to tag
newsworthy events in our calendar.
REM Jan 18 2020 TAG letter \
MSG Date to see the new Jumanji movie
-s
and filter for only those items:
rem -s 2020-01-01 |
awk '$3 ~ /letter/'
RUN
directive:
REM 1 MSF [iif($Tm % 2, "Honey", "Hubby")] \
writes %"Grandparent letter%"
REM 1 RUN rem -sr [$U - 1] | \
awk '$3 ~ /letter/{for (i=2; i<6; i++) $i=""; print}'
-r
prevents
remind
from executing
RUN
entries further,
and the expression
[$U - 1]
finds the last day of the previous month
to pass to
remind
)
Thus, not only do I get a reminder
to let me know who writes the letter
but I get a list of the letter-worthy things
we did in the previous month.
I wish
remind
had more native functionality
to support tags
such as a
trigtags()
function
(to return the tags
for the current reminder),
or a
hastag()
function
(to ask if the current reminder
has a given tag).
This would help me
filter output
or expand tags in message-bodies.
Alas, not yet.
I look forward to some day (hopefully)
when these are even more useful.
Perhaps for my next patch.
Shell tips & tricks
Aliases
I've set up a couple
bash
aliases
in my shell to minimize typing:
alias 1='rem -gaad -iCOLOR=1'
for d in {2..6} ; do
alias ${i}='rem -gaad -iCOLOR=1 "*"'${i}
done
3
as a command
and get 3 days
of colorized agenda output.
Crontab
I have
crontab
entries that email me my
today/tomorrow reminders each day
and a full week of reminders on Sunday morning:
@daily rem -g '*2'
* * 0 rem '*7'
Version control
I find it useful to keep my reminder directory,
~/.config/remind/
,
in version control.
I use
git
for multiple files,
but you could just as easily use
mercurial
,
Subversion
,
or even
CVS
or
rcs
.
This gives me a lot of freedom
to experiment with complex reminder syntax.
If I mess something up,
I can always revert to the known-good state.
Also, I can delete reminders that have passed
so they don't clutter my calendar
or increase processing time,
but still have historical record of their existence.
I can also
git push
&
git pull
changes between different machines
to keep my reminders in sync.
cd ~/.config/remind
git init .
git add *.rem
git commit -m "Initial checkin"
modify reminder files
git commit -am "Added John's birthday"
cd
mkdir RCS
echo "Initial checkin" | ci -l .reminders
ed -p"* " .reminders
3141
a
REM Apr 1 MSG Tom's birthday%
.
wq
3170
echo "Added Tom's birthday" | ci -l .reminders
Speech
The readable nature of the output
makes it easy to pipe output to a program like
espeak
(or another text-to-speech engine
which you would also have to install)
to read your reminders aloud.
You might create a custom
spoken.rem
file that you don't
INCLUDE
in your regular
reminders.rem
in which you remove the
BANNER
,
emit section dividers
(if you don't choose to use
display labels),
and
INCLUDE
a select subset of calendars:
BANNER %
MSG Birthdays%
INCLUDE [filedir()]/birthdays.rem
MSG Personal items%
INCLUDE [filedir()]/tim.rem
MSG Work items%
INCLUDE [filedir()]/work.rem
[filedir()]
expands to the path of the containing
spoken.rem
file)
And to read them:
remind ~/.config/remind/spoken.rem | espeak
Easier or more boring recipes
Car registration
We need to get our car inspected
and get our (re)registration submitted
by the end of February
so I want notifications starting at the beginning of February:
REM Mar 1 --1 ++[26 + isleap($Uy)] \
SATISFY [$Uy > 2019] \
MSG %"Car reg./inspection due%" %b
Mar 1 --1
results in the last day of February
while the
++[26 + isleap($Uy)]
gives 26 or 27 days of advanced warning
depending on whether the current year
is a leap year.
Once I've done the registration/inspection,
I update the year in the
SATISFY
so it stops reminding me about it for that year.
Maturing bonds
I purchased a couple simple-interest bonds
and want to know when to expect the interest payment checks.
They arrive
around the 25th of the month
(or
AFTER
if it falls on a Sunday
when the USPS doesn't deliver mail),
every 6 months
until the end of the bond-term.
But
remind
makes this pretty easy:
REM 25 UNTIL Feb 26, 2025 \
OMIT Sun AFTER \
SATISFY [$Tm % 6 == 2] \
MSG %"Expect bond check for $31.41%"
SATISFY
keyword,
repeatedly looking for the next
25th of the month
until
$Tm % 6 == 2
— the month in question
is either February or August.
Notice until action
I send birthday cards
to certain friends & family members
so I want about a week of notice
to get a card,
write a note,
and get it in the mail.
However, once I've sent the card,
I don't need further
advanced warning reminders.
By updating that year in the expression
once I've completed the task
I no longer get advanced warning for the given year.
Similar to the
car registration
example,
in this case I still want to receive notification
on the day
of
the birthday too,
not drop the reminder entirely.
FSET last_sent(y) iif($Uy > y, 7, 0)
# once I've sent a card to Grandma in 2020
# I'll change the "2019" to "2020":
REM Jul 6 +[last_sent(2019)] MSG %"Grandma%" %b
+
"
for Grandma's notification
because sometimes holidays interfere with the mail
so I appreciate having the extended notice.
Messages tweaked based on date
As a Christmas gift to our grandparents,
we take turns hand-writing a letter to them each month.
So on the first of each month,
I want a reminder,
but also want to know whose turn:
REM 1 MSF [iif($Tm % 2, "Honey", "Hubby")] \
writes %"Grandparent letter%"
REM Sat MSF Scrub %"[ \
choose((weekno() % 3)+1, \
"Kitchen", \
"Master", \
"Kids'") \
] grout%"
Selectively getting advanced notice
For friends I see at church on Sunday
I would like advance notice of their birthday on Sunday
but don't want additional reminders
during the intervening days of the week.
For this, I use an expression evaluation
to dynamically determine the number of days worth of notice:
FSET church_notice() iif($Uw == 0, 6, 0)
REM Jan 16 ++[church_notice()] MSG %"Tony %c%"
Annoying credit-reporting company rules
I learned the hard way that the credit-reporting companies
will not let you order your credit report on the same day each year
because a full year hasn't actually passed.
Yes, I'd love to pull my free credit report on the same day of each year.
However, I need to pull them
more
than 365 days (366 on a leap-year) apart.
So I use the repeat modifier
to get reminded every 367 days.
I also intersperse them throughout the year
rather than all at once
so that I get a more frequent sampling in case something has gone wrong:
REM Jan 1 2019 *367 MSF Order credit report: %"Equifax%" \
(1-877-322-8228)%_\
https://www.annualcreditreport.com/index.action
REM May 1 2019 *367 MSF Order credit report: %"Experian%" \
(1-877-322-8228)%_\
https://www.annualcreditreport.com/index.action
REM Sep 13 2019 *367 MSF Order credit report: %"TransUnion%" \
(1-877-322-8228)%_\
https://www.annualcreditreport.com/index.action
Post-holiday candy sales
I tend to stock up on chocolate
when it goes on sale after some of the big candy holidays.
(I keep stashes of chocolate hidden around the house
so that when my beloved has a hankering for a certain type,
I can often play the miracle-worker
and bring it out of hiding).
So I use
remind
to make sure I don't forget:
REM Feb 15 MSG %"Post-Valentine candy sales%"%
REM [easterdate($Uy) + 1] MSG %"Post-Easter candy sales%"%
REM Nov 1 MSG %"Post-Halloween candy sales%"%
REM Dec 26 MSG %"Post-Christmas candy sales%"%
remind
does this without much thought at all
thanks to the built-in
easterdate()
function.
The rest wouldn't pose any trouble
for an ordinary calendar program.
Did any of these reminders trigger?
Sometimes you have several events
and you want to provide some
SPECIAL
modifier for all of them.
You can use
remind
's
special
$NumTrig
variable to check this:
SET n $NumTrig
REM Sat Sun SATISFY 1
REM Feb 14 MSG Valentine's Day
REM [easterdate()] MSG Easter
IF $NumTrig > n
# At least one triggered
REM SPECIAL SHADE 75
ENDIF
$NumTrig
,
then the
IF
will allow for the
SPECIAL SHADE
to happen for the day in question.
FizzBuzz
Often in interviews
the interviewer will instruct
the candidate to whiteboard the classic
fizzbuzz
solution where the program emits
n
numbers but
if the number is divisible by 3
print "fizz",
if the number is divisible by 5
print "buzz",
and if the number is divisible by 3 & 5
print "fizzbuzz"
(otherwise just printing the number).
So in case you ever need to do this using
remind
:
FSET doy() $U - date($Uy-1, 12, 31)
MSG [iif(doy() % 15 == 0, \
"fizzbuzz", \
iif(doy() % 5 == 0, \
"buzz", \
iif(doy() % 3 == 0, \
"fizz", \
doy() \
) \
) \
)]
doy()
function returns the Julian day of the year
(as found by taking the current date,
$U
,
and subtracting December 31st
of the previous year)
and then uses that to calculate
the fizzbuzz-ness of the day.
Dumb? Yep.
Useless? You bet.
Fun demonstration? Why not.
More complex calendar recipes
Shifting garbage day
The go-to for demonstrating the power of
remind
involves shifting your garbage collection day back
if a holiday occurs earlier in the week:
PUSH
CLEAR
INCLUDE [filedir()]/holidays.rem
SET garbday 4
SET garbdayname wkday(garbday)
FSET _garbhol(x) wkdaynum(x) == garbday \
&& slide(x - garbday, garbday) != x
FSET _garbdel() iif($Tw != garbday, " (Delayed)", "")
REM [garbdayname] +1 AFTER OMITFUNC _garbhol \
MSG %"Garbage day[_garbdel()]%" %b%
POP
garbday
value to your garbage day
(Monday = 1, Friday = 5)
and set up your holiday
OMIT
entries in your
holiday.rem
(or hard-code them inside the
PUSH
/CLEAR
/POP
block)
and
remind
will automatically shift the garbage day reminder
based on the holiday schedule.
Pretty date-diff
While
remind
lets you subtract two dates
to get the number of days between them,
sometimes you want that difference
expresses as a number of years, months, and days.
To do that,
I have this
ymd_diff
function which returns
values like
"3y 2m 11d"
or
"3y 1m 14d ago"
which you might find more useful:
FSET ymd_diff_gt(d1, d2) \
iif(year(d2) > year(d1), \
(year(d2) - year(d1)) - ( \
(monnum(d2) < monnum(d1)) \
|| \
(monnum(d2) == monnum(d1) && day(d2) < day(d1)) \
), \
0 \
) + "y " + \
iif(monnum(d2) > monnum(d1), \
(monnum(d2) - monnum(d1)) - (day(d2) < day(d1)), \
iif(monnum(d2) < monnum(d1), \
(12 + monnum(d2) - monnum(d1)) - (day(d2) < day(d1)), \
iif(day(d2) < day(d1), 11, 0) \
)) + "m " + \
iif(day(d2) >= day(d1), \
day(d2) - day(d1), \
(daysinmon(monnum(d1), year(d2)) + day(d2)) - day(d1) \
) + "d"
FSET ymd_diff(d1, d2) \
iif(d1 > d2, ymd_diff_gt(d2, d1) + " ago",\
ymd_diff_gt(d1, d2) \
)
Generating reminder files programmatically
One of my calendars gives me a daily
man
-page
to read based on my list of system
man
-pages
looking something like
SET ManStart date(2020,1,1)
REM [ManStart + 0] MSG man 5 %"fstab%"%
REM [ManStart + 1] MSG man 1 %"rs%"%
REM [ManStart + 2] MSG man 1 %"awk%"%
REM [ManStart + 3] MSG man 6 %"cribbage%"%
cat remify_manpages.awk
BEGIN {
print "SET ManStart date(2020,1,5)"
}
$7 ~ /^[1678]$/ {
printf("REM [ManStart + %i] MSG man %s %%\"%s%%\"%%\n", NR - 1, $7, $6)
}
find /usr/share/man/man[1678]/ -maxdepth 2 -type f \
| grep -vi perl \
| sort -R \
| awk -F'[./]' -f remify_manpages.awk > manpages.rem
man
files in
/usr/share/man/man[1678]/
removing the many
perl
-related pages I don't care about,
shuffles them so I get a nice variety,
then uses the
awk
script to transform them into a
remind
file.
Now I have several years worth
of daily man-pages to read.
If you have a pre-existing list of dates,
you can use the standard suite of Unix tools
to transform the list
into something that
remind
can use.
Relative date calculations for event-planning
I serve on the leadership team
for a local non-profit
that works to provide clothing-vouchers
to kids on the school free/reduced-lunch program.
However the calendar changes each year
based on certain fixed dates including
when the school counselors have their training before school starts,
when school starts,
and which weekends the shopping event(s) take(s) place.
I use
SET
to define the fixed-dates,
and calculate the other dates based off them.
SET CounselorTraining date(2019,7,31)
SET SchoolStart date(2019,8,15)
SET Event date(2019,10,19)
SET ApplicationsGoHome (Event - 47)
REM [CounselorTraining] MSG %"Attend counselor training%"
REM [SchoolStart] MSG %"First day of school%"
REM [ApplicationsGoHome - 3] \
MSG %"Remind counselors applications will be going home%"
REM [ApplicationsGoHome] \
MSG %"Applications go home with kids%"
REM [ApplicationsGoHome] \
MSG %"School calls families to let them know to watch for applications%"
REM [Event - 37] \
MSG %"Remind counselors that applications must be back [Event - 33]%"
REM [Event - 33] MSG %"Check funding levels%"
REM [Event - 32] MSG %"Pick up applications from school%"
REM [Event - 32] THROUGH [Event - 26] MSG %"Data entry%"
REM [Event - 10] MSG %"Print acceptance letters%"
REM [Event - 8] MSG %"Email counselors to expect letters%"
REM [Event - 8] MSG %"Acceptance letters go home%"
REM [Event - 5] MSG %"Email volunteers a reminder%"
REM [Event] MSG %"Event%"
REM [Event + 2] MSG %"Email volunteers to thank%"
OMIT
and
BEFORE
in there too to jostle things around a bit)
I can then use
rem -p3 2019-08-01 | rem2ps | ps2pdf - calendar.pdf
to generate the calendar PDF
that I can email out to the rest of the leadership team.
Medicine schedule
One of our kids takes a medication that we receive in a 30-day supply. This means we need to pick it up at the pharmacy at least the day before to ensure uninterrupted dosing. The pharmacy occasionally needs a business day or two to get the medication in, so I like to drop off the prescription a couple days early. Which means I need to pick up the prescription at the doctor's office before that. Which means I need to call the doctor's office 72hr before that. And all of those dates can get shifted by weekends and holidays. On a less-powerful calendar, I'd schedule a reminder every 30 days and then hope I remember to take weekends and holidays into consideration.
On the other hand,
remind
makes this fairly straightforward to express.
While doing this,
I'd also like to get
nagging reminders
and advanced notice
if I haven't yet picked up the pills.
But once I have,
I don't want continued reminders.
First we'll begin
by creating an
OMIT
context to hold the holidays
that the pharmacy & doctor's office close.
PUSH
CLEAR
# the Dr. and pharmacy close on these dates
OMIT Dec 24
OMIT Dec 25
# reminders will go here
POP
SET MostRecentlyGotPills date(2020, 1, 24)
# Started taking the medication on
SET MedStartDate date(2019, 3, 18)
# we get 30 pills
SET MedFreq 30
# Dr. office requests 72hr of notice
# but remind needs it in days
SET RXLeadTime (72/24)
REM [MedStartDate] *[MedFreq] SATISFY 1
SET PillsRunOut trigdate()
SATISFY 1
produces no output,
but sets the most recent trigger date.
Using the
trigdate()
function,
you can capture the date of the most recent event
for use in further calculations
and later reuse.
So those lines determine
the next day we'll run out of pills.
From here,
we know that we need to pick up the pills
the day before then
(possibly more than one day before
if there's an intervening holiday or weekend):
# need to pick up meds at least one day before we run out
REM [PillsRunOut] -1 +1 \
OMIT Sun BEFORE \
SATISFY [$U > MostRecentlyGotPills] \
MSG %"Pick up pills%" at pharmacy %b
-1
").
Because this uses the single minus,
it will skip over
OMIT
days, including holidays
that close the pharmacy & doctor's office,
as well as Sundays when the pharmacy is closed.
The
SATISFY
ensures that,
if we have picked up the pills
on or after the date currently under consideration,
we don't bother reminding about this.
I also like a little advanced warning
(+1
).
In case life gets busy,
I have a little more room to
fit in a trip to the pharmacy.
Once we know when we need to pick up the pills,
we can capture that date
(again, with
trigdate()
)
and use it to determine
when we need to
pick up the prescription
and take it to the pharmacy
(the day before,
but the doctor's office
doesn't open for prescriptions
on weekends or over holidays
so it might get bumped back
further).
SET RealPillPickUpDate trigdate()
REM [RealPillPickUpDate - 1] +1 \
OMIT Sat Sun BEFORE \
SATISFY [$U > MostRecentlyGotPills] \
MSF %"Pick up Rx%" at Dr. %b \
and take to pharmacy
SET RealRxPickUpDate trigdate()
REM [RealRxPickUpDate] -[RXLeadTime] \
OMIT Sat Sun BEFORE \
SATISFY [$U > MostRecentlyGotPills] \
MSF %"Call Dr. office to request Rx refill%" %b
SATISFY
to ensure we only get this
if we haven't already picked up
the most recent batch of pills.
This produces a final series
of variables and reminders
that notify me at appropriate times
and do all the holiday/weekend math
so we don't accidentally end up
without medication:
################
# Update here: #
################
SET MostRecentlyGotPills date(2020, 1, 24)
###################
# Configure here: #
###################
SET MedStartDate date(2019, 11, 27)
# we get 30 pills
SET MedFreq 30
# Dr. office requests 72hr of notice
# but remind needs it in days
SET RXLeadTime (72/24)
PUSH
CLEAR
# the Dr. and pharmacy close on these dates
OMIT Jan 1
OMIT Dec 24
OMIT Dec 25
#########################
# when's the next occurrence?
REM [MedStartDate] *[MedFreq] SATISFY 1
SET PillsRunOut trigdate()
# need to pick up meds at least one day before we run out
REM [PillsRunOut] -1 +1 \
OMIT Sat Sun BEFORE \
SATISFY [$U > MostRecentlyGotPills] \
MSG %"Pick up pills%" at pharmacy %b
SET RealPillPickUpDate trigdate()
REM [RealPillPickUpDate - 1] +1 \
OMIT Sat Sun BEFORE \
SATISFY [$U > MostRecentlyGotPills] \
MSF %"Pick up Rx%" at Dr. %b \
and take to pharmacy
SET RealRxPickUpDate trigdate()
REM [RealRxPickUpDate] -[RXLeadTime] \
OMIT Sat Sun BEFORE \
SATISFY [$U > MostRecentlyGotPills] \
MSF %"Call Dr. office to request Rx refill%" %b
POP
MostRecentlyGotPills
with the new start-date.
You can use the same template
adjusting the
MostRecentlyGotPills
,
MedStartDate
,
MedFreq
,
and
RXLeadTime
,
for your use case.
Scanning forward from an earlier date
Sometimes you have an
OMIT
that has happened recently
and would thus impact how
advanced notice with
+
and
adjusting dates with
BEFORE
/AFTER
/SKIP
calculations occur,
but because
remind
first determines the
OMIT
dates based on the "today" in question,
it might not adjust properly.
For example,
consider a floating holiday like Labor Day
(the first Monday in September):
REM Mon 1 Sep SATISFY 1
OMIT [$T] MSG Labor Day
SCANFROM
,
we encounter the right date:
REM Mon 1 Sep SCANFROM [$U-7] SATISFY 1
OMIT [$T] MSG Labor Day
# Start looking for July 4th
# beginning 7 days ago:
REM 4 July SCANFROM [$U - 7] SATISFY 1
# if it fell on a Saturday
# observed on the preceding Friday
IF wkdaynum(trigdate()) == 6
REM [trigdate()] MSG Independence day (actual)
OMIT [trigdate()-1] MSG Independence day (observed)
ELSE
# if it fell on a Sunday
# observed on the following Monday
IF wkdaynum(trigdate()) == 0
REM [trigdate()] MSG Independence day (actual)
OMIT [trigdate()+1] MSG Independence day (observed)
ELSE
# otherwise, observed=actual
OMIT [trigdate()] MSG Independence day
ENDIF
ENDIF
defs.rem
)
School N-day cycles ignoring holidays & weekends
Sometimes you want to know the number of days
between two dates,
without counting holidays
or other
OMIT
dates.
This often shows up in school schedules
where a 4- or 6-day cycle
often rotates across the standard 5-day week.
The
nonomitted()
function lets you do this
with minimal fuss.
However, the start-date
(the first parameter)
must
be less-than or equal to the end-date or
remind
will produce an error.
SET FirstDayOfSchool date(2019, 8, 14)
# a 4-day cycle
SET SchoolPhase 4
PUSH
CLEAR
# list of school holidays
# and in-service days:
OMIT Jan 1
OMIT Dec 23 2019 THROUGH Jan 6 2020 \
SATISFY [$Uw > 0 && $Uw < 6] \
MSG %"Christmas: No school%"%
OMIT Mar 13 2020 MSG %"In-service: No school%"%
IF $U >= FirstDayOfSchool
SET DayNumber nonomitted( \
FirstDayOfSchool, \
$U, \
"SAT", "SUN" \
)
SET schedule DayNumber % SchoolPhase + 1
REM Aug 13 2021 AT 7:15 THROUGH May 21 2020 \
SKIP \
OMIT Sat Sun \
MSG %"School%" %2 ([choose( \
schedule, \
"music", \
"art", \
"language", \
"health" \
)])%
ENDIF
Daemon mode
In addition to invoking
remind
manually or from scripts,
remind
also offers a "daemon mode"
that will run in the background
and fire off commands as
AT
events come around.
remind -z5 -k'espeak %s &' ~/.reminders
remind
which will check for changes in your
~/.reminders
every 5 minutes.
If an
AT
passes,
remind
will spawn
espeak
passing the reminder-text
(shell-escaped)
in place of the
%s
so that the reminder text gets spoken.
Alternatively you could use
xmessage
or
zenity
to pop up a dialog
remind -z5 -k'xmessage %s &' ~/.reminders
xmessage
dialog boxes)
or use
notify-send
to display the reminder-text
as a notification:
remind -z5 -k'notify-send %s &' ~/.reminders
MSG
contents on a 3d-printer.
Endless possibilities.
Your reminder file can use the
$Daemon
variable to test to see if
remind
is running in daemon mode:
REM Jan 5 2020 AT 10:30 MSG [ \
iif($Daemon, "Hey, you! ", "") \
] Doctor's appointment
espeak
to speak your reminders
Other utilities
Generating iCalendar/.ics
files
Great.
Now you have all your reminders
generating just the way you want.
How do you share them with other people?
Most other
cough*inferior*cough
calendar programs still support
.ics
format files to describe events.
You can use the
rem2ics
utility to convert
the output of
remind
into a
.ics
file that you can import
into Google Calendar,
Outlook,
or iCalendar.
Note:
While it will export all of your reminders
including repeating reminders,
it will not include enough meta-information
to preserve things like repeats,
so the events show up in the external calendar
as individual events.
rem -s12 |
TZ=CST8CDT rem2ics -do >reminders.ics
reminders.ics
file verbatim,
and having the wrong timezone
can cause lots of problems.
Other external utilities
Outside the scope of this article
you'll find things like
tkremind
(a GUI front-end to
remind
)
and
wyrd
(a TUI front-end to
remind
).
I don't currently use either of these
so I'll leave those
for the reader to explore.
Troubleshooting & debugging
While the vast majority of my reminders pose no issues, occasionally one ends up complex enough that I want to make sure it really works like I expect it to. Because these really are code, I appreciate having some tools to debug them.
Echoing into
remind -
As a first line of debugging,
I usually try to isolate the single reminder
and then run
remind
with the date I want to test.
If you specify a filename of
"-
",
remind
will read reminders from
stdin
which lets you
echo
a single reminder
to test it:
echo 'REM 1 --1 MSG Last' |
remind - 2020-1-31
echo
statements in parentheses:
(echo 'OMIT Dec 31'
echo 'REM 1 --1 MSG Last'
) | remind - 2020-12-31
echo REM 1 --1 MSG Last \
remind - 2020-12-31 '*33'
for m in {1..12}
do
echo "REM 1 --1 MSG Last" |
remind - 2020-${m}-30
done
Using a custom
.reminders
file
Sometimes you really do need a lengthier test case.
For these, I back-up my
~/.reminders
file,
cp ~/.reminders{,_orig}
$EDITOR .reminders # create test case
mv ~/{.,working_}reminders
mv ~/.reminders{_orig,} # restore the original
$EDITOR
to copy the working bits from
~/working_reminders
into my reminder file.
Alternatively, I also have my machines
configured with a "guest" user,
so sometimes it ends up faster
to log in as the "guest"
(who has no
~/.reminders
file of consequence),
blow away any previous test
~/.reminders
file,
and test there on an isolated case
without risk to my
real
reminders.
Showing LOTS of dates
Sometimes I find it easiest to
look at a spew of calendars
and eyeball the results.
I like to do this in
month view,
producing a whole year
and paging through the results with my
$PAGER
(less
in my case).
rem -c12 -w$COLUMNS 2020-1-1 | less
-p
/-s
flags to dump
all the reminders in a easy-to-filter format.
rem -p12 2020-1-1 | less
Printing values in
MSG
output
In the same classic tradition of
printf()
debugging,
I also find it useful to see what
remind
thinks a variable holds:
SET SomeVar some complex expression
REM … MSG SomeVar=[SomeVar] on [$T]/[$U]
Debugging flags
For really getting under the hood
remind
has a
-d
option that turns on additional debugging output.
It gets verbose quickly,
but invoking
rem -dextvlf 2>&1 | less
man
-page
details each of those options,
but it has occasionally helped me
spot something I would have otherwise missed.
I find the
x
(tracing expression evaluation),
t
(tracing trigger-date computation),
and
v
(dumping all variables)
help the most.
Wrap up
When I first heard about
remind
it sounded interesting,
but I tried multiple times
before it finally clicked for me
(much like
git
which took me about three serious attempts
before I felt like I understood it).
But once I figured out that I could express things
that no other calendar program let me do,
I have trouble keeping my primary calendar
in anything else.
I use
rsync
and
git
to keep my calendar in sync
across multiple machines
without exposing my calendar info
to third-parties
like Google
or Microsoft.
Hopefully this post gives you the motivation
to evaluate it for your hardest calendar test-cases.
And if you come up with
strange questions,
use-cases,
or other recipes
for using
remind
feel free to contact me via
email,
on the
remind
mailing list,
or
over on Twitter
@gumnos
and I'll do my best
to help out.