kal.c: A Resilient Terminal Calendar
The Philosophy of kal
The standard BSD calendar utility is a testament to the Unix philosophy: it reads plain text files, matches dates, and prints strings. It is simple, unpretentious, and enduring.
However, real life is rarely bound entirely by fixed dates like "Jan 16." Real life is routines, daily habits, weekday work blocks, and rolling anniversaries. I built kal.c to maintain the strict text-as-data ethos of the original BSD tool while injecting modern pragmatism: custom repetition macros, dynamic age calculation, and context filtering, all without requiring complex cron syntax or external preprocessors.
Here is a code audit that explains how the engine works, block by block.
The Code, Explained
1. Dependencies and String Manipulation
C requires you to build your own tools for handling text. These helper functions ensure our calendar file is resilient against typos, case-shifting, and invisible trailing spaces.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
#include <stdbool.h>
#include <limits.h>
/* Helper to convert a string to lowercase for case-insensitive matching */
void str_tolower(char *str) {
for (; *str; ++str) {
*str = tolower((unsigned char)*str);
}
}
/* Helper to strip the newline character from the end of a string */
void strip_newline(char *str) {
str[strcspn(str, "\n")] = 0;
}
The Rationale: When you edit a text file quickly, it is easy to type JAN one day and Jan the next, or accidentally leave a trailing space before the tab. str_tolower and the strip helpers guarantee that the matching engine evaluates raw, clean data regardless of how hastily it was typed.
2. The Dynamic %AGE% Macro
Plain text is inherently static, but time is not. If I write down a birthday or a system installation date, I do not want to update the file every year.
/* Helper to replace the first instance of %AGE% with a calculated integer */
void replace_age(char *str, int age) {
char *pos = strstr(str, "%AGE%");
if (pos) {
char temp[1024];
int prefix_len = pos - str;
strncpy(temp, str, prefix_len);
temp[prefix_len] = '\0';
sprintf(temp + prefix_len, "%d%s", age, pos + 5);
strcpy(str, temp);
}
}
The Rationale: This function scans the event description for the exact string %AGE%. If found, it splices the calculated integer into the text buffer before printing. A line written as 1959-01-16 Birthday (Turning %AGE%) will dynamically print out as "Turning 67" or "Turning 68" depending on the year the command is run.
3. Target Dates and Time Math
To simulate running the calendar on future dates, we need a way to parse continuous numeric strings (like 250913 for September 13, 2025) and forcefully override the system clock.
/* Helper to parse -t string in [[[cc]yy]mm]dd format and override target date */
bool set_target_date(const char *date_str, struct tm *target) {
int len = strlen(date_str);
int cc = -1, yy = -1, mm = -1, dd = -1;
for (int i = 0; i < len; i++) {
if (!isdigit((unsigned char)date_str[i])) return false;
}
if (len == 2) sscanf(date_str, "%2d", &dd);
else if (len == 4) sscanf(date_str, "%2d%2d", &mm, &dd);
else if (len == 6) sscanf(date_str, "%2d%2d%2d", &yy, &mm, &dd);
else if (len == 8) sscanf(date_str, "%2d%2d%2d%2d", &cc, &yy, &mm, &dd);
else return false;
target->tm_mday = dd;
if (mm != -1) target->tm_mon = mm - 1;
if (yy != -1) {
if (cc != -1) target->tm_year = (cc * 100 + yy) - 1900;
else {
if (yy >= 69) target->tm_year = yy;
else target->tm_year = yy + 100;
}
}
target->tm_isdst = -1;
if (mktime(target) == -1) return false;
return true;
}
The Rationale: Standard POSIX behavior dictates that 2-digit years below 69 pivot into the 2000s, while 69 and above assume the 1900s. The true magic here is the final mktime(target). By passing our mutated struct back to C's standard time library, it automatically recalculates the day of the week (tm_wday). This ensures that if we simulate a Thursday, our M-F weekday rules still trigger correctly.
4. The Matching Engine
This is the heart of kal.c. It evaluates the string preceding the first tab character against the target date.
/* Core matching logic for a specific single day. Extracts year if available. */
bool matches_today(char *date_str, struct tm *target_day, int *out_year) {
char lower_date[64];
strncpy(lower_date, date_str, sizeof(lower_date) - 1);
lower_date[sizeof(lower_date) - 1] = '\0';
char *end = lower_date + strlen(lower_date) - 1;
while (end > lower_date && isspace((unsigned char)*end)) {
*end = '\0';
end--;
}
str_tolower(lower_date);
// 1. Custom Repetition Macros
if (strcmp(lower_date, "* *") == 0) return true;
if (strcmp(lower_date, "m-f") == 0) return (target_day->tm_wday >= 1 && target_day->tm_wday <= 5);
if (strcmp(lower_date, "sasun") == 0) return (target_day->tm_wday == 0 || target_day->tm_wday == 6);
// [Standard BSD matching blocks omitted for brevity: handles MM/DD, Jan 16, etc.]
// ...
The Rationale: The classic BSD tool lacked a way to handle simple repetition. Rather than implementing a full cron parser, kal introduces three human-readable strict strings:
* *- Triggers every single day (for daily reading goals or system checks).
M-F- Triggers Monday through Friday.
SaSun- Triggers strictly on weekends.
5. The Window Loop
If you only see today's tasks, you can't plan. kal uses a looping window to look forward and backward in time.
/* Loop through the window. Returns the integer offset of days (e.g. 1 for tomorrow). */
int find_matching_offset(char *date_str, struct tm *base_target, int lookbehind_days, int lookahead_days, int *out_year) {
for (int i = -lookbehind_days; i <= lookahead_days; i++) {
struct tm day_to_check = *base_target;
day_to_check.tm_mday += i;
day_to_check.tm_isdst = -1;
if (mktime(&day_to_check) != -1) {
int matched_year = -1;
if (matches_today(date_str, &day_to_check, &matched_year)) {
if (out_year) *out_year = matched_year;
return i;
}
}
}
return INT_MAX;
}
The Rationale: Date math across month boundaries (e.g., looking three days ahead from February 27th on a leap year) is notoriously complex. By simply adding an integer offset (i) to tm_mday and letting mktime normalize the struct, the C standard library handles all leap-year and month-rollover edge cases flawlessly. Returning the actual integer offset allows the main loop to generate dynamic tags like [Tomorrow].
6. The Main Loop & Output Formatting
The final piece parses the command-line arguments, reads the file, applies filters, and formats the output.
// [Inside the fgets while-loop...]
// Apply the -g grep filter to the event description
if (filter_tag && strstr(tab_pos + 1, filter_tag) == NULL) {
*tab_pos = '\t';
continue;
}
int event_year = -1;
int offset = find_matching_offset(buffer, target, lookbehind_days, lookahead_days, &event_year);
if (offset != INT_MAX) {
// Calculate and replace %AGE% macro
if (event_year != -1) {
int age = (target->tm_year + 1900) - event_year;
replace_age(tab_pos + 1, age);
}
// Determine weekday
int matched_wday = (target->tm_wday + offset) % 7;
if (matched_wday < 0) matched_wday += 7;
// Format the relative countdown tag
char countdown[64] = "";
if (offset == 1) snprintf(countdown, sizeof(countdown), " [Tomorrow]");
else if (offset == -1) snprintf(countdown, sizeof(countdown), " [Yesterday]");
else if (offset > 1) snprintf(countdown, sizeof(countdown), " [In %d days]", offset);
else if (offset < -1) snprintf(countdown, sizeof(countdown), " [%d days ago]", -offset);
// Print Output
if (print_wday) printf("%s ", day_names[matched_wday]);
if (print_rule) {
*tab_pos = '\t';
printf("%s%s\n", buffer, countdown);
} else {
printf("%s%s\n", tab_pos + 1, countdown);
}
}
The Rationale:
- The Grep Filter (-g): I want a single, centralized plain-text file for everything—coding, writing, crafts, and home maintenance. The simple
strstrcheck allows me to run./kal -g "@writing"and instantly isolate my publishing tasks without maintaining separate files. Relative Countdowns: Standard terminal outputs can be an impenetrable wall of text. By dynamically appending
[Tomorrow]or[In 3 days]to the lookahead matches, the output becomes instantly scannable, reducing cognitive load when evaluating the week ahead.
Text passages copyright © 2026 by William Wear. All rights reserved. All code provided herein is free and unencumbered software released into the public domain under The Unlicense. For more information please refer directly to https://unlicense.org/.