From: Johannes Thyssen Tishman Subject: Re: cal: add option to highlight the current day To: Job Snijders Cc: tech@openbsd.org, landry@openbsd.org, patrick keshishian Date: Sun, 5 Jul 2026 10:38:19 +0000 2026-07-04T09:41:36+0000 Job Snijders : > Hi Johannes, > > Thanks! I've longed for this functionality for some time now. Some > comments below. Thanks for the feedback Job. I've answered a couple of your comments inline and addressed all of them in the patch below. Is this better? > On Fri, Jul 03, 2026 at 04:37:29PM +0000, Johannes Thyssen Tishman wrote: > > Please find below a patch to add an option (-h) to cal to allow > > highlighting the current day. > > > > For testing purposes, the -h flag takes an optional argument specifying > > the date to highlight, e.g., cal -h2026-10-10 oct. I intend to remove > > this before committing if the patch is accepted. > > Since the program tests whether standout highlighting can be used, to > me it would make most sense to highlight the current day by default. > Highlighting of arbitrary days seems less useful. I suggest remove '-h' > (bonus: it reduces the size of patch). The optional argument of the -h flag was mostly so testers could check that the highlight was working for other days other than the current one without having to change the system date. As mentioned above, my intention was to drop this before committing. I've now updated the patch to highlight the current day by default. However, this now conflicts with what Patrick wants. Can we find a compromise here? I'm not sure I like an option to *disable* the highlight. I'd prefer an option to *enable* it or make it the default and have no option at all. > [...] > > void > > +highlight_day(char *p, int day, int *hl_len) > > +{ > > + const char *term_so, *term_se; > > + char cbuf[512]; > > + char tbuf[1024], *b; > > + > > + term_se = term_so = NULL; > > + > > + /* > > + * why NULL as name here? > > + * termcap(3) doesn't seem document this > > + */ > From termcap(3): > > The tgetstr routine returns the string entry for id, or zero if it is not > available. Use tputs to output the returned string. The area parameter > is used as follows: > > * It is assumed to be the address of a pointer to a buffer managed > by the calling application. > > * However, ncurses checks to ensure that area is not NULL, and also > that the resulting buffer pointer is not NULL. If either check > fails, the area parameter is ignored. > > * If the checks succeed, ncurses also copies the return value to the > buffer pointed to by area, and the area value will be updated to > point past the null ending this value. > > * The return value itself is an address in the terminal description > which is loaded into memory. Right, this is why I'm asking about the use of cbuf and b, which are not used here or in the FreeBSD implementation, instead of just setting area to NULL for tgetstr (I've now done this in the revised patch below). However, I still don't understand why NULL for the second parameter of tgetent. AFAIC, that is not described in termcap(3). > > + if (tgetent(tbuf, NULL) == 1) { > > + b = cbuf; > > + > > + /* > > + * why not NULL here for area instead of &b? > > + * cbuf is never used > > + */ > > + term_so = tgetstr("so", &b); > > + term_se = tgetstr("se", &b); > > + } Index: usr.bin/cal/Makefile =================================================================== RCS file: /cvs/src/usr.bin/cal/Makefile,v diff -u -p -r1.3 Makefile --- usr.bin/cal/Makefile 21 Sep 1997 11:48:29 -0000 1.3 +++ usr.bin/cal/Makefile 5 Jul 2026 10:35:50 -0000 @@ -1,5 +1,6 @@ # $OpenBSD: Makefile,v 1.3 1997/09/21 11:48:29 deraadt Exp $ -PROG= cal +PROG= cal +LDADD+= -lcurses .include Index: usr.bin/cal/cal.c =================================================================== RCS file: /cvs/src/usr.bin/cal/cal.c,v diff -u -p -r1.35 cal.c --- usr.bin/cal/cal.c 28 Jun 2026 20:46:53 -0000 1.35 +++ usr.bin/cal/cal.c 5 Jul 2026 10:35:51 -0000 @@ -42,6 +42,8 @@ #include #include #include +#include +#include #define THURSDAY 4 /* for reformation */ #define SATURDAY 6 /* 1 Jan 1 was a Saturday */ @@ -127,8 +129,14 @@ const char *day_headings = NULL; int julian; int mflag = 0; int wflag = 0; +int highlight = 0; +int highlight_len = 0; +const char *term_so = NULL; +const char *term_se = NULL; +struct tm *lt; void ascii_day(char *, int); +void highlight_day(char *, int); void center(const char *, int, int); void day_array(int, int, int *); int day_in_week(int, int, int); @@ -141,15 +149,18 @@ void trim_trailing_spaces(char *); void usage(void); void yearly(int); int parsemonth(const char *); +int is_today(int, int, int); +int can_highlight(void); int main(int argc, char *argv[]) { - struct tm *local_time; time_t now; int ch, month, year, yflag; const char *errstr; + highlight = can_highlight(); + if (pledge("stdio", NULL) == -1) err(1, "pledge"); @@ -190,6 +201,13 @@ main(int argc, char *argv[]) day_headings = DAY_HEADINGS_JS; } + (void)time(&now); + lt = localtime(&now); + if (lt == NULL) { + perror("localtime"); + exit(1); + } + month = 0; switch(argc) { case 2: @@ -198,9 +216,7 @@ main(int argc, char *argv[]) case 1: if (argc == 1 && !isdigit((unsigned char)*argv[0])) { month = parsemonth(*argv); - (void)time(&now); - local_time = localtime(&now); - year = local_time->tm_year + 1900; + year = lt->tm_year + 1900; } else { year = strtonum(*argv, 1, 9999, &errstr); if (errstr) @@ -208,11 +224,9 @@ main(int argc, char *argv[]) } break; case 0: - (void)time(&now); - local_time = localtime(&now); - year = local_time->tm_year + 1900; + year = lt->tm_year + 1900; if (!yflag) - month = local_time->tm_mon + 1; + month = lt->tm_mon + 1; break; default: usage(); @@ -294,8 +308,13 @@ isoweek(int day, int month, int year) void monthly(int month, int year) { - int col, row, len, days[MAXDAYS], firstday; - char *p, lineout[30]; + int col, row, len, hl_len, d, days[MAXDAYS], firstday; + + /* + * At least 9 bytes for standout mode escape sequence. What is a safe + * size for this? Could this differ from terminal to terminal? + */ + char *p, lineout[48]; day_array(month, year, days); (void)snprintf(lineout, sizeof(lineout), "%s %d", @@ -306,15 +325,22 @@ monthly(int month, int year) lineout, day_headings); for (row = 0; row < 6; row++) { firstday = SPACE; + hl_len = 0; for (col = 0, p = lineout; col < 7; col++, p += julian ? J_DAY_LEN : DAY_LEN) { - if (firstday == SPACE && days[row * 7 + col] != SPACE) - firstday = days[row * 7 + col]; - ascii_day(p, days[row * 7 + col]); + d = days[row * 7 + col]; + if (firstday == SPACE && d != SPACE) + firstday = d; + if (is_today(d, month, year) && highlight) { + highlight_day(p, d); + hl_len = highlight_len; + p += hl_len; + } else + ascii_day(p, d); } *p = '\0'; trim_trailing_spaces(lineout); - (void)printf("%-20s", lineout); + (void)printf("%-*s", 20 + hl_len, lineout); if (wflag && firstday != SPACE) printf(" [%2d]", week(firstday, month, year)); printf("\n"); @@ -324,8 +350,13 @@ monthly(int month, int year) void j_yearly(int year) { - int col, *dp, i, month, row, which_cal; + int col, *dp, i, month, row, which_cal, hl_len; int days[12][MAXDAYS]; + + /* + * At least 9 bytes for standout mode escape sequence. What is a safe + * size for this? Could this differ from terminal to terminal? + */ char *p, lineout[80]; (void)snprintf(lineout, sizeof(lineout), "%d", year); @@ -342,15 +373,32 @@ j_yearly(int year) J_HEAD_SEP, "", day_headings); for (row = 0; row < 6; row++) { + hl_len = 0; for (which_cal = 0; which_cal < 2; which_cal++) { p = lineout + which_cal * (J_WEEK_LEN + 2); + p += hl_len; dp = &days[month + which_cal][row * 7]; - for (col = 0; col < 7; col++, p += J_DAY_LEN) - ascii_day(p, *dp++); + for (col = 0; col < 7; col++, p += J_DAY_LEN) { + if (is_today(*dp, month + which_cal + 1, + year) && highlight) { + highlight_day(p, *dp++); + hl_len = highlight_len; + p += hl_len; + } else + ascii_day(p, *dp++); + } } *p = '\0'; trim_trailing_spaces(lineout); (void)printf("%s\n", lineout); + + /* + * Highlighting a day causes escape sequences to + * misalign the rows. Rather than tracking offsets to + * overwrite rows day-by-day, clear the row. + */ + if (highlight) + (void)memset(lineout, ' ', sizeof(lineout) - 1); } } (void)printf("\n"); @@ -359,9 +407,14 @@ j_yearly(int year) void yearly(int year) { - int col, *dp, i, month, row, which_cal, week_len, wn, firstday; + int col, *dp, i, month, row, which_cal, week_len, hl_len, wn, firstday; int days[12][MAXDAYS]; - char *p, lineout[81]; + + /* + * At least 9 bytes for standout mode escape sequence. What is a safe + * size for this? Could this differ from terminal to terminal? + */ + char *p, lineout[96]; week_len = WEEK_LEN; if (wflag) @@ -382,15 +435,23 @@ yearly(int year) HEAD_SEP + (wflag ? WEEKNUMBER_LEN : 0), "", day_headings); for (row = 0; row < 6; row++) { + hl_len = 0; for (which_cal = 0; which_cal < 3; which_cal++) { p = lineout + which_cal * (week_len + 2); + p += hl_len; dp = &days[month + which_cal][row * 7]; firstday = SPACE; for (col = 0; col < 7; col++, p += DAY_LEN) { if (firstday == SPACE && *dp != SPACE) firstday = *dp; - ascii_day(p, *dp++); + if (is_today(*dp, month + which_cal + 1, + year) && highlight) { + highlight_day(p, *dp++); + hl_len = highlight_len; + p += hl_len; + } else + ascii_day(p, *dp++); } if (wflag && firstday != SPACE) { wn = week(firstday, @@ -404,6 +465,14 @@ yearly(int year) *p = '\0'; trim_trailing_spaces(lineout); (void)printf("%s\n", lineout); + + /* + * Highlighting a day causes escape sequences to + * misalign the rows. Rather than tracking offsets to + * overwrite rows day-by-day, clear the row. + */ + if (highlight) + (void)memset(lineout, ' ', sizeof(lineout) - 1); } } (void)printf("\n"); @@ -510,6 +579,23 @@ ascii_day(char *p, int day) } void +highlight_day(char *p, int day) +{ + /* highlight on */ + memcpy(p, term_so, strlen(term_so)); + p += strlen(term_so); + + /* the actual text */ + ascii_day(p, day); + p += (julian ? J_DAY_LEN : DAY_LEN) - 1; + + /* highlight off */ + memcpy(p, term_se, strlen(term_se)); + p += strlen(term_se); + *p = ' '; +} + +void trim_trailing_spaces(char *s) { char *p; @@ -556,4 +642,29 @@ parsemonth(const char *s) if (v <= 0 || v > 12) errx(1, "invalid month: use 1-12 or a name"); return (v); +} + +int +is_today(int day, int month, int year) +{ + return (year == lt->tm_year + 1900 && month == lt->tm_mon + 1 && + day == (julian ? lt->tm_yday + 1 : lt->tm_mday)); +} + +int +can_highlight(void) +{ + char tbuf[1024]; + + if (!isatty(STDOUT_FILENO)) + return 0; + if (tgetent(tbuf, NULL) == 1) { + term_so = tgetstr("so", NULL); + term_se = tgetstr("se", NULL); + } + if (term_so != NULL && term_se != NULL) { + highlight_len = strlen(term_so) + strlen(term_se); + return 1; + } + return 0; }