Skip to content

bicknell/today-web

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

today-web

Live demo

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.

Technologies

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, see fonts/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 from today36-dosbox-1947-birthday-1.png rather 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 (.screen for text at native size, .canvas for the aspect-corrected size, .stage for 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.

Screen Layout

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 year DDDD is 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, red DDDD year 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.

Design and Implementation

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.1today.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.

Language

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.

One core, two drivers

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 cron job 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, so days/ 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 fixed YEAR=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 days runs core/today once per date and redirects its output straight into days/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.

The core program

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.mdstruct 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() + manual fopen/unlink with tmpfile() — not deprecated, and shorter than what it replaced.
  • Fixed printf(buffer) → proper format strings.
  • Fixed the if (event); stray-semicolon bug documented in BUILDING.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 \r on the end of every message, throwing off column-width padding. Fixed by stripping both \r and \n right 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.

Data source

zips/TODAY36.ZIP in the research repo — the actual v3.6 shareware release the reference screenshots were taken from. Its TODAY.JANTODAY.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).

Makefile

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.

Page layout

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).

Pagination

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.

DOS prompt / boot screen

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 a C:\> prompt with a blinking _ cursor (CSS steps() animation on a plain underscore, not a font glyph). The MMDD field is pre-filled with today's date (client-side Date()), 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 MMDD character 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.

Hosting

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.

About

A web implementation of Patrick Kincaid's TODAY v3.6, a DOS-era trivia program originally circulated via BBS.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages