remind-rb
Ruby bindings for Remind, through Fiddle.
require "remind"
Remind.today = Date.new(2026, 8, 19)
reminder = Remind.parse("REM Mon 13 SKIP OMIT Sat Sun MSG payday")
reminder.summary # => "payday"
reminder.occurrences.first(3) # => Remind's own trigger dates
reminder.weekdays # => ["MO"]
Remind.evaluate("moonphase(today())") # => 46
Remind.evaluate("trigger(today() + 7)") # => "26 August 2026"
Why
Remind's trigger language is bigger than it looks. REM Mon 13 is not the
second Monday of the month, it is the Monday of the week the 13th falls in.
REM Wed Thu 15 fires once, on whichever of those weekdays comes first on or
after the 15th. Then there are SKIP, BEFORE, AFTER, OMIT, SCANFROM,
UNTIL, *n repeats and back-counted deltas — the manual works through
eighteen examples before it reaches the exceptions.
Every program that has read reminder files with regular expressions has got
part of it wrong. These bindings do not read it at all: ParseRem parses the
trigger, ComputeTrigger says which dates it fires on, and DoSubst renders
the message. What is on the Ruby side is a translation of names, and nothing
about the language.
Layout
| Path | What it is |
|---|---|
remind-v06.02.10/ |
the Remind release these bindings bind, vendored unchanged |
ext/remind/shim.c |
the only C in the gem: struct accessors, and the sequence in DoRem |
lib/remind/builder.rb |
builds the sources into a shared library, with the flags ./configure chose |
lib/remind/native.rb |
the dlopen'd library: functions, globals, constants |
lib/remind/runtime.rb |
the startup InitRemind does before it looks at argv |
lib/remind/session.rb |
a live interpreter: today =, evaluate |
lib/remind/reminder.rb |
one REM line: its fields, its message, its dates |
lib/remind/source.rb |
a reminder file, with continuations joined and INCLUDEs followed |
.github/workflows/ |
cross-compile.yml builds the platform gems; test.yaml runs the suites |
lib/rem2ics/, exe/rem2ics |
the converter — see docs/rem2ics.md |
lib/remlint/, exe/remlint |
RemLint, a style linter for reminder files — see docs/remlint.md |
Three gems share this lib/, the way ratalada's three share theirs:
remind-rb, rem2ics and remlint, each with its own gemspec at the root and
its own version, all developed from one bundle.
Installing
gem install remind-rb
That is an unpack, not a build. Remind is C, and compiling it means
./configure, make and a toolchain — a packaging problem, solved once in CI
rather than on every machine that installs the gem.
.github/workflows/cross-compile.yml builds the shared library on each
platform and packages a gem with the library already inside; RubyGems serves
the one matching yours. The library is built without readline, so it asks the
loader for nothing a plain system lacks.
The source gem carries the vendored Remind sources and no library, for a
platform CI does not cover: gem install remind-rb --platform=ruby then
rake compile. REMIND_LIBRARY points the bindings at a library built
anywhere else.
Working on it
bin/setup # bundle, compile, install git hooks
rake compile # ./configure + one -fPIC -shared compile
bin/test # every suite in the repository
rake native[x86_64-linux] # package a precompiled gem for one platform
The compile reads the compiler, the flags, the feature macros and the
libraries to link out of the src/Makefile that ./configure generated, and
adds -fPIC -shared and ext/remind/shim.c. It writes
lib/remind/libremind.so — where a precompiled gem carries it, so a checkout
and an installed gem look the same to everything above.
Versioning
6.2.10.0 is Remind 06.02.10, packaged for the first time. The first three
segments say which Remind is inside; the fourth is ours, bumped for changes on
the Ruby side against that same release:
bin/increment-version # 6.2.10.0 -> 6.2.10.1
bin/increment-version 06.03.00 # track a newly vendored release -> 6.3.0.0
The second form refuses to name a release whose sources are not vendored, so the version cannot claim a Remind that is not there.
Releasing
CI builds the platform gems and uploads them; publishing is deliberate:
bin/release-gem
It refuses to publish a version that is not ahead of what is on RubyGems,
builds the source gem locally, downloads the precompiled gems from the latest
green cross-compile.yml run, and pushes all of them — tolerating anything
already published, so a half-finished release can simply be re-run.
The two things to know
Fiddle, not a C extension. There is nothing to extend: Remind's sources
compile into a shared library as they are, and everything worth calling is a
plain function over ints and pointers. ext/shim.c exists for two things
Fiddle cannot do — reading a 30-field struct without hard-coding byte offsets,
and running the six-call sequence DoRem uses to parse a line — not because
Fiddle needed help calling anything.
The state is process-wide. Remind keeps "today" in a global, because a
program that runs once and exits has no reason not to. Remind.today = moves
the ground under every calculation, deliberately: a reminder that says Mon
means a different date tomorrow than it does today, and pinning the date is
what makes a conversion reproducible. Calls into the library are serialised
behind one lock, whether they came from one Session or several.
Licence
GPL-2.0-only — see LICENSE.
Not a choice so much as a consequence: this gem vendors Remind's complete
source tree, compiles it, and ships the result, so what it publishes is a
distribution of Remind. ext/remind/shim.c is written against Remind's own
headers and links into one shared library with its objects. Remind is
Copyright (C) 1992-2026 Dianne Skoll; the bindings around it are Copyright (C)
2026 Nathan Kidd, under the same terms.