A web implementation of Patrick Kincaid's TODAY v3.6, a DOS-era trivia program originally circulated via BBS.
Goal: reproduce the screen output of the old DOS today program as native
web content (HTML/CSS/text), instead of as static screenshots.
To build and preview locally: make && make server, then visit
http://localhost:8000/ — see "Makefile" below for details.
The DOS program renders an 80x25 character-mode screen using the IBM PC BIOS font (CP437) and standard 16-color CGA/EGA/VGA attributes, including ANSI-style box-drawing graphics. That look is replicated in the browser using:
- A pixel-perfect web font replicating the original IBM VGA ROM glyphs:
fonts/Web437_IBM_VGA_9x16.woff, from int10h.org's "Oldschool PC Font Pack" v2.2 (CC BY-SA 4.0, seefonts/LICENSE-fonts.txt) — the pixel-outline ("Web437") variant of the genuine IBM VGA ROM font, at its native 9x16 size, including the CP437 box-drawing and block characters. - The program's output laid out as a
<pre>-based text block, with foreground/background colors applied via light inline markup around color runs (see "One core, two drivers" below) rather than a full 80x25 attribute-per-cell grid. Colors sampled directly fromtoday36-dosbox-1947-birthday-1.pngrather than the nominal CGA/EGA 16-color palette, since the DOSBox capture doesn't actually render at those nominal values: background#051CA2; white#FFFFFF; yellow#FFFF50; cyan#51FFFF; red#E3485B(yellow and cyan nudged ~10% brighter than the raw sample to match how they read on screen). - Disabled font antialiasing / pixelated rendering, to keep the crisp,
blocky look of the original font at native size. The font is rendered
at its native pixel height or an exact integer multiple of it (16px,
32px, ...) — per int10h's own documentation, this applies to the font
itself and to anything else (like the scanline overlay below) whose
spacing needs to line up with individual font pixels. Any further
resizing is done by scaling the enclosing box with a CSS transform,
never by setting a fractional
font-size— getting this wrong caused two rounds of moiré/banding artifacts in the scanline overlay before landing on the current three-layer structure (.screenfor text at native size,.canvasfor the aspect-corrected size,.stagefor the final on-screen zoom, all tied together with a shared CSS custom property so the scanline period stays correct if the zoom level changes). - VGA text mode's 720x400 framebuffer stretched to a 4:3 display, giving
non-square pixels (~0.74 pixel aspect ratio, i.e. a ~1.35x vertical
stretch relative to the font's native square-pixel glyphs), applied
as a CSS
transform: scaleY(1.35).
Reference screenshots (e.g. today36-dosbox-1947-birthday-1.png, zoomed
into a plain blue fill area) also show a CRT scanline artifact: solid
color fills render as fine horizontal bands with a thin dark gap between
them, at the same vertical period as a single row of font pixels — not a
per-character-cell period. Reproduced as a separate CSS overlay (a
repeating-linear-gradient of thin dark stripes) layered across the
whole screen, with the stripe period tuned to one font-pixel row so it
lines up with the underlying glyph raster — confirmed final.
The CGA/EGA "blink" attribute is covered by the DOS prompt's blinking
cursor (.cursor, a CSS steps() animation on a plain underscore, not
a font glyph) — no separate attribute emulation needed.
Documented from the TODAY36 DOSBox screenshots in the research repo
(screenshots/today36-dosbox-1947-birthday-1.png and -2.png), which
show the two screens of a single query (June 28) that overflows one
80x25 page.
- Background: blue, for the entire 80x25 screen.
- A single-line white border, drawn with CP437 box-drawing characters, runs around the full screen. This border and the two rows inside it (header + separator, below) are redrawn on every page, including the "Press ENTER for more..." continuation page.
- Row 1 inside the border: a single white header line with fixed
spacing:
" TODAY Version 3.6 11/14/93 Copyright 1986, 1993 By Patrick Kincaid ". - Row 2 inside the border: another single-line white border, separating the header from the body.
- Body starts with white text:
"Famous events for <Month DD>:", followed by a blank line. - Birthday section: yellow
"Happy Birthday to...", then one cyan line per entry, indented 3 spaces. Where an entry starts with"In DDDD", the yearDDDDis red, the rest of the line cyan. - Blank line between sections.
- "On this day" section: same pattern — yellow
"On this day..."header, cyan body lines indented 3 spaces, redDDDDyear when the line starts with"In DDDD". (Not every line has a leading year — e.g."Pons-Winnecke meteor shower, radiant in Draco"has none.) - Blank line, then thought section: yellow
"Thought for the day..."header, followed by a single cyan line, indented 3 spaces (e.g."The heart is wiser than the intellect."). - Blank line, then closing section: yellow
"And remember..."header, followed by a single cyan line, indented 3 spaces (e.g."Pay the mortgage!"). - Pagination: if all of the above doesn't fit in the remaining
screen rows, the program stops mid-content and prints white
"Press ENTER for more..."in place of whatever comes next, then a blank line, then the bottom border — the rest of the frame (down to the border) is left as plain blue background. In the reference screenshots this cut lands right after the yellow"Thought for the day..."header, deferring its body line and the whole "And remember..." section to a second page. - End of content: once everything has been printed (no more
pagination needed), the program instead prints a white prompt in that
same position:
"Enter a new date as MMDD or just <ENTER> to quit:", with the cursor left after it awaiting input — this is the interactive date-reentry loop mentioned below. Everything below the prompt is blank blue background down to the bottom border.
This mid-content pagination break is replicated in the browser — see "Page layout" below.
This is not a screen-capture/conversion project — there's no DOS
session to record and replay. It's a from-scratch reimplementation, in
the same spirit as the other independent today ports documented in the
research repo (Hugo Fiennes's ARCbbs
module, Mick Howland's CP/M port, Rebecca Heineman's KitchenSink C++
port): a new program that reads the same today.1–today.12 data
files and produces its own rendering, styled to closely match the look of
the DOS TODAY36 release (the last and most polished DOS version —
color text, drawn border, interactive date-reentry loop) without having
any of Kincaid's original source to work from.
C, to continue the project's own lineage (Kincaid's original,
hfiennes/arcbbs's c/today, and this research repo's own today.c
are all C/C++). The one place untrusted input enters the program is the
MMDD [YYYY] command-line argument — parsed with bounded sscanf,
never passed to a shell.
A self-hosted server capable of running an annual cron job would be needed for the Cron driver — but see "Hosting" below: the actual deployment target is GitHub Pages, which is static-only, so that driver isn't part of the current build. The core itself still only ever runs locally/in CI, as a build step that generates the static files Batch produces — never as a deployed server-side binary, for this target.
A single core function parses a requested date against the data files and emits TODAY36-style output: the same section structure ("Happy Birthday to...", "On this day...", "Thought for the day...", "And remember..."), the same box-drawn border (CP437 box-drawing characters, since that's how the DOS original actually drew it — not a CSS-drawn border).
The output format is a <pre class="screen">...</pre> fragment:
<span class="brd">, <span class="hdr">, <span class="sec">,
<span class="body">, and <span class="yr"> runs for the four color
classes plus border, and nothing else — no nesting, no attributes
beyond class. This is the format the outer page fetches and inserts
into the DOM by a small amount of JavaScript (see "Page layout" below).
Text pulled from the today.N data files is HTML-escaped (&, <, >) before being
wrapped in a span — that data is decades of externally mailed-in trivia
submissions, not code the project controls. So the core does need to know
it's emitting valid HTML (matched tags, escaping) — it still doesn't need
to know anything about the actual CSS colors those class names resolve
to, which live entirely in the outer page's stylesheet.
Two thin drivers share that core, though only Batch is needed for the current GitHub Pages target (see "Hosting" below) — Cron stays a valid, unimplemented option for a future self-hosted deployment:
- Cron: the same Batch process below, re-run periodically by a
system
cronjob on a self-hosted server instead of once at commit time — e.g.make days YEAR=$(date +%Y)scheduled early on January 1st each year, sodays/tracks the current year (correctly resolving Labor Day, the DST reminders, etc. against it — see "Data source" below) instead of staying pinned to GitHub Pages' intentionally fixedYEAR=1993. - Batch: loops all 365/366 dates (including Feb 29), writing one file
per day to
days/(e.g.days/0628.html), committed to the repo. Each file bundles all of that day's pages — one<pre class="screen">block per page, in order — rather than one file per page; see "Pagination" below for why. Implemented:make daysrunscore/todayonce per date and redirects its output straight intodays/MMDD.html— a single invocation of the core already emits every page for that date in one stream, so no separate bundling step is needed. See "The core program" and "Makefile" below.
core/today.c is a from-scratch reimplementation, but
built as a minimal-diff extension of the original Unix reader recovered
in the research repo,
per that repo's own BUILDING.md —
struct line, the months[] table, the record-parsing sscanf call,
and the single-pass gather-into-a-buffer-per-section approach are all
structurally unchanged.
Modernized, to compile clean with zero warnings under
-Wall -Wextra -Wpedantic on both clang and gcc-15, at -std=c89 and
-std=c99 — no -Wno-* flags, unlike the original (verified by
BUILDING.md's own documented flag list):
- Added the missing
<stdlib.h>,<string.h>,<ctype.h>. - Replaced
tmpnam()+ manualfopen/unlinkwithtmpfile()— not deprecated, and shorter than what it replaced. - Fixed
printf(buffer)→ proper format strings. - Fixed the
if (event);stray-semicolon bug documented inBUILDING.md's "Known bug" section. This is the one substantive behavior change beyond pure modernization: there was no way to satisfy "compiles clean without-Wno-empty-body" and keep that bug.
Added (none of this existed in the original reader, which just
dumped section text with no formatting): the header banner, the
box-drawn border (UTF-8 box-drawing characters), the HTML color spans
described in "One core, two drivers" above, HTML-escaping, and
pagination —
PAGEBUDGET = 18 content lines per page, reverse-engineered from the
exact row counts in the two reference screenshots (page 1 cuts after 18
lines; page 2's content ends and the closing prompt follows immediately,
with no padding row between them).
A fourth record type, F (Fortune/Thought-for-the-day), read from
today.wit. The original reader only handles B/S/R — it has no
"Thought for the day" section at all. today.wit uses the exact same
record format as the monthly files, so this needed no new parsing logic,
just a second file scanned the same way.
Two encoding bugs found and fixed while testing against the real data, neither of which the original reader had to care about since it just echoed bytes to a DOS console that was already CP437-native:
- CRLF line endings. The original assumed LF-only input (true of the
previously-recovered
today.1–.12, but not of DOS-native text files); reading CRLF data left a stray\ron the end of every message, throwing off column-width padding. Fixed by stripping both\rand\nright after the message is captured. - CP437, not UTF-8. Bytes 0x80–0xFF in the data are accented Latin
letters, box-drawing, and symbols in the standard IBM PC OEM codepage
— not ASCII, not UTF-8. Passed through raw, they produced mojibake in
the browser (caught via "Boötes", the constellation name in a January
1 entry, rendering as a replacement character). Fixed with a 128-entry
CP437→UTF-8 table in
htmlescape().
Tested against all 366 calendar dates (including Feb 29): no crashes, and a page-count distribution of 134 one-page days, 229 two-page days, and 3 three-page days (January 1, July 1, July 4) — see "Pagination" above.
zips/TODAY36.ZIP
in the research repo — the actual v3.6 shareware release the reference
screenshots were taken from. Its TODAY.JAN–TODAY.DEC, TODAY.WIT,
and TODAY.EXE are the real thing. core/data/ holds these files
renamed to the today.N convention the reader expects, producing
output that matches the reference screenshots exactly for birthdays,
events, and the thought of the day.
Resolved: today.own and the reminder date-range format.
TODAY36.ZIP also has a TODAY.OWN (personal reminders, separately
searched alongside the monthly files — confirmed in TODAY36.DOC),
copied into core/data/today.own and now read by today.c alongside
the monthly files and today.wit. Its entries don't use the simple
format TODAY36.DOC's own example shows (R0001 Pay the mortgage!,
cols 6–9 blank) — the real entry is R00270004 Pay the mortgage!, cols
6–9 holding a second MMDD (0004), which the doc says means "a range
of dates on which the reminder will display" without a worked example of
the exact semantics (it promises a "Special Reminder Format" section
that, checked directly against TODAY36.DOC, doesn't actually exist —
a dangling reference in the original doc, not just a gap in our
research). Working through every entry actually in today.own settled
it: cols 6–9 are an inclusive range end date, and two shapes occur —
- Both start and end month wildcarded (
R00270004): a day-of-month window that recurs every month, wrapping over the month boundary (day 27 through day 4 of the next month) — this is what puts "Pay the mortgage!" on the June 28 reference screenshot. - Both start and end month concrete (e.g.
R09170923, "Dad's Birthday September 23rd"): an ordinary (month, day) range, Sep 17–23.
A range with only one end wildcarded doesn't occur anywhere in the data
and isn't documented, so matches() in today.c deliberately never
matches that shape rather than guessing. Implemented and verified
against every boundary (range start/end inclusive, month wraparound,
overlapping reminders on the same date all rendering together).
Also resolved along the way: column 10's day-of-week gating. Several
records (the DST reminders in today.3/today.4/today.10, and
TODAY36.DOC's own Labor Day example in today.9) use column 10 for a
day-of-week digit (1=Sunday…7=Saturday) meaning "only show on this
weekday" — e.g. today.4 has seven near-identical "Spring Forward"
candidates for April 1–7, only one of which should actually display in
a given year. This was never implemented (only the C-continuation
special char was), so all seven were showing every year. Now
implemented in matches(), computing the weekday via Sakamoto's
algorithm — but that needs a concrete year to resolve "the first Sunday
in April" against, and the data files themselves are year-agnostic, so
today now takes the year as an explicit, optional second argument:
today [MMDD [YYYY]] (no arguments: current date and year from the
system clock; MMDD alone: given month/day, current system year;
both: fully explicit). The Makefile's days target passes a YEAR
variable, defaulted to 1993 (the year TODAY 3.6 itself shipped in,
per the "11/14/93" in its own banner) but overridable —
make days YEAR=2026 regenerates days/ against a different
reference year without touching the source. Verified against Labor Day
(correctly resolves to September 6, 1993 and September 2, 2024 — the
respective first Mondays) and all three DST clusters (each shows on
exactly one day, for whatever year is given).
At the repo root. make (or make all) builds core/today from
core/today.c. make days builds it if needed and then runs it once
per calendar date (all 365 plus Feb 29 = 366), redirecting each
invocation's output to days/MMDD.html, passing a YEAR variable
(default 1993 — see "Data source" above) as today's day-of-week
reference year; override with make days YEAR=2026 to regenerate
against a different year. make clean removes both the binary and the
generated days/ directory. make server runs python3 -m http.server
on a PORT variable (default 8000) to serve the repo root for local
testing — index.html's fetch('days/MMDD.html') needs a real origin,
which file:// URLs can't give it in most browsers.
One static index.html: a DOS-monitor-styled frame in the middle (the
pixel-perfect CP437 font block described in "Technologies" above),
surrounded by ordinary modern-font content describing the project, with
a date-entry field and Go/Enter buttons. CSS and JS both live in their
own separate files (today-web.css, today-web.js), not inline.
On load, JS computes today's date client-side and fetches days/MMDD.html
for it, then inserts the first <pre class="screen"> block found in the
response. If the file contains more than one block, only the first is
shown initially — see "Pagination" below.
A small footer below the monitor credits the two things this project depends on but didn't create: the font (linking to int10h.org's "Oldschool PC Font Pack") and the data/history/research this whole project is downstream of (linking to bicknell/today).
Some dates need more than one page — June 28 already does, in the
reference screenshots. Across all 366 calendar dates, 134 need one page,
229 need two, and three — January 1, July 1, and July 4 — need
three. Since a day's whole file is fetched at once (see "One core,
two drivers" above), the Enter button doesn't need a second network
round-trip to advance to the next page: it just hides the current
<pre class="screen"> block and shows the next one already sitting in
the DOM. This is also why no separate JSON manifest of page counts is
needed — the page count for a given day is just how many blocks came back
in that day's fetch.
Resolved: what the Enter button does on a day's last page. Rather
than trying to port the DOS original's "enter a new date or press ENTER
to quit" prompt literally (there's no "quit" on a persistent web page),
the last page's Enter instead returns the whole monitor to a black
C:\> DOS prompt screen — see "DOS prompt / boot screen" below.
The monitor doesn't jump straight to a day's output; it starts (and returns to, after a day's last page) a plain black DOS prompt screen, giving the whole page a "boot to prompt, run the program, exit back to prompt" feel instead of a single static display:
- First-ever load: shows the full MS-DOS boot banner —
Starting MS-DOS.../Microsoft(R) MS-DOS(R) Version 6.00/(C)Copyright Microsoft Corp 1981-1993.— matching the MS-DOS 6.00 boot screen (the out-of-the-box default as of March 1993; DOS 6.22 didn't ship until 1994, a year too new for the 1993 reference year — see "Data source" above) — above aC:\>prompt with a blinking_cursor (CSSsteps()animation on a plain underscore, not a font glyph). The MMDD field is pre-filled with today's date (client-sideDate()), but nothing auto-loads — the user has to act. - Go (wired as the form's submit button, so pressing Return in the
field works too): types out
TODAY.EXE MMDDcharacter by character at the prompt, pauses briefly, then swaps the screen to that day's first page. - Enter advances through a day's pages (see "Pagination" above);
on the last page, it returns to a bare
C:\>— no banner, since that only belongs on an actual boot, not a re-run. On the idle prompt screen, Enter does nothing. - Clicking Go again while output is showing returns to the bare prompt first, then retypes the new command — so switching dates always goes through the same "type the command" beat, not a hard cut.
Target: GitHub Pages, serving this repo
statically — no server-side execution of any kind. Note that
raw.githubusercontent.com doesn't work for this: GitHub deliberately
serves raw file content as Content-Type: text/plain (an anti-XSS
measure), so a .html file loaded from there shows as page source, not a
rendered page. GitHub Pages serves the same files with correct content
types instead. A .nojekyll file at the repo root skips the unnecessary
Jekyll build step.
GitHub Pages can't execute anything server-side, not even PHP — but
that's not actually why days/ is pinned to a fixed reference year
(YEAR=1993, see "Data source" above) rather than kept current. That's
a deliberate choice: the whole point is that nothing needs to run on
GitHub's side, ever. days/ is generated once, committed, and served
statically forever after — a platform that could run scheduled jobs
wouldn't change that decision. That doesn't change the core's design
(see "One core, two drivers" above), which stays driver-agnostic on
purpose; it just means only the Batch driver is actually needed to ship
this. Cron remains a valid option for a self-hosted deployment that
wants days/ to track the current year instead — see "One core, two
drivers" above.