Add $TerminalHyperlinks system variable.

This lets you turn any reminder with an INFO "Url: ..." string
into a hyperlink on the terminal.
This commit is contained in:
Dianne Skoll
2026-01-08 14:13:34 -05:00
parent 3e6233b6f0
commit f6253d0fca
14 changed files with 141 additions and 52 deletions

View File

@@ -3488,6 +3488,13 @@ to be "dark" if the average of the red, green and blue components is
at most 85 out of 255, and if the maximum of any component is at most at most 85 out of 255, and if the maximum of any component is at most
128 out of 255. 128 out of 255.
.TP .TP
.B $TerminalHyperlinks (INT type)
If your terminal supports escape sequences to allow HTML-like
anchors around text (see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda), then you can set this variable to 1. \fBRemind\fR will then
make any reminder with a "Url:" info string into a hyperlink in your
terminal. By default, \fB$TerminalHyperlinks\fR is set to zero because
not all terminals support this feature.
.TP
.B $WarningLevel (STRING type) .B $WarningLevel (STRING type)
As new versions of \fBRemind\fR are released, new warnings may be added. As new versions of \fBRemind\fR are released, new warnings may be added.
If your formerly-fine scripts suddenly start issuing warnings when you If your formerly-fine scripts suddenly start issuing warnings when you

View File

@@ -95,9 +95,6 @@ static struct line_drawing UTF8Drawing = {
"\xe2\x94\x80" "\xe2\x94\x80"
}; };
static char const *start_link = "\x1B]8;;";
static char const *end_link = "\x1B]8;;\x1B\\";
static char *VT100Colors[2][2][2][2] /* [Br][R][G][B] */ = { static char *VT100Colors[2][2][2][2] /* [Br][R][G][B] */ = {
{ {
/*** DIM COLORS ***/ /*** DIM COLORS ***/
@@ -1578,8 +1575,7 @@ static int WriteOneColLine(int col)
} }
if (url) { if (url) {
printf("%s", start_link); printf("\x1B]8;;%s\x1B\\", url);
printf("%s\x1B\\", url);
} }
/* If we couldn't find a space char, print what we have. */ /* If we couldn't find a space char, print what we have. */
if (!wspace) { if (!wspace) {
@@ -1616,7 +1612,7 @@ static int WriteOneColLine(int col)
} }
if (url) { if (url) {
printf("%s", end_link); printf("\x1B]8;;\x1B\\");
} }
/* Decolorize reminder if necessary, but keep any SHADE */ /* Decolorize reminder if necessary, but keep any SHADE */
if (UseVTColors && e->is_color) { if (UseVTColors && e->is_color) {
@@ -2445,7 +2441,7 @@ get_url(TrigInfo *infos)
{ {
TrigInfo *ti = infos; TrigInfo *ti = infos;
char const *url; char const *url;
if (!LinksInTerminal) { if (!TerminalHyperlinks) {
/* Nope, not doing links in terminal */ /* Nope, not doing links in terminal */
return NULL; return NULL;
} }

View File

@@ -1555,6 +1555,7 @@ int TriggerReminder(ParsePtr p, Trigger *t, TimeTrig const *tim, int dse, int is
DynamicBuffer pre_buf; DynamicBuffer pre_buf;
char const *s; char const *s;
char const *msg_command = NULL; char const *msg_command = NULL;
char const *url;
Value v; Value v;
if (MsgCommand) { if (MsgCommand) {
@@ -1830,9 +1831,16 @@ int TriggerReminder(ParsePtr p, Trigger *t, TimeTrig const *tim, int dse, int is
} }
} }
/* If we are sorting, just queue it up in the sort buffer */ /* Get the url if terminal hyperlinks are enabled */
if (TerminalHyperlinks) {
url = FindTrigInfo(t, "url");
} else {
url = NULL;
}
/* If we are sorting, just queue it up in the sort buffer */
if (SortByDate) { if (SortByDate) {
if (InsertIntoSortBuffer(dse, tim->ttime, DBufValue(&buf), if (InsertIntoSortBuffer(dse, tim->ttime, url, DBufValue(&buf),
t->typ, t->priority) == OK) { t->typ, t->priority) == OK) {
DBufFree(&buf); DBufFree(&buf);
NumTriggered++; NumTriggered++;
@@ -1855,14 +1863,20 @@ int TriggerReminder(ParsePtr p, Trigger *t, TimeTrig const *tim, int dse, int is
if (IsServerMode() && !strncmp(DBufValue(&buf), "NOTE endreminder", 16)) { if (IsServerMode() && !strncmp(DBufValue(&buf), "NOTE endreminder", 16)) {
printf(" %s", DBufValue(&buf)); printf(" %s", DBufValue(&buf));
} else { } else {
if (url) {
printf("\x1B]8;;%s\x1B\\", url);
}
printf("%s", DBufValue(&buf)); printf("%s", DBufValue(&buf));
if (url) {
printf("\x1B]8;;\x1B\\");
}
} }
} }
} }
break; break;
case MSF_TYPE: case MSF_TYPE:
FillParagraph(DBufValue(&buf), output); FillParagraph(url, DBufValue(&buf), output);
break; break;
case RUN_TYPE: case RUN_TYPE:

View File

@@ -106,7 +106,6 @@ EXTERN INIT( int DefaultPrio, NO_PRIORITY);
EXTERN INIT( int SysTime, -1); EXTERN INIT( int SysTime, -1);
EXTERN INIT( int LocalSysTime, -1); EXTERN INIT( int LocalSysTime, -1);
EXTERN INIT( int ParseUntriggered, 0); EXTERN INIT( int ParseUntriggered, 0);
EXTERN INIT( int LinksInTerminal, 0);
EXTERN char const *InitialFile; EXTERN char const *InitialFile;
EXTERN char const *LocalTimeZone; EXTERN char const *LocalTimeZone;
@@ -171,6 +170,10 @@ EXTERN INIT( double Latitude, DEFAULT_LATITUDE);
EXTERN INIT( char *Location, LOCATION); EXTERN INIT( char *Location, LOCATION);
/* Support hyperlinks in terminal emulators?
https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
*/
EXTERN INIT( int TerminalHyperlinks, 0);
/* UTC calculation stuff */ /* UTC calculation stuff */
EXTERN INIT( int MinsFromUTC, 0); EXTERN INIT( int MinsFromUTC, 0);
EXTERN INIT( int CalculateUTC, 1); EXTERN INIT( int CalculateUTC, 1);

View File

@@ -519,11 +519,6 @@ void InitRemind(int argc, char const *argv[])
arg++; arg++;
continue; continue;
} }
if (*arg == 'z' || *arg == 'Z') {
LinksInTerminal = 1;
arg++;
continue;
}
break; break;
} }
if (weeks) { if (weeks) {

View File

@@ -1866,7 +1866,7 @@ FillParagraphWC(char const *s, DynamicBuffer *output)
/* A macro safe ONLY if used with arg with no side effects! */ /* A macro safe ONLY if used with arg with no side effects! */
#define ISBLANK(c) (isspace(c) && (c) != '\n') #define ISBLANK(c) (isspace(c) && (c) != '\n')
void FillParagraph(char const *s, DynamicBuffer *output) void FillParagraph(char const *url, char const *s, DynamicBuffer *output)
{ {
int line = 0; int line = 0;
@@ -1883,7 +1883,14 @@ void FillParagraph(char const *s, DynamicBuffer *output)
while(ISBLANK(*s)) s++; while(ISBLANK(*s)) s++;
if (!*s) return; if (!*s) return;
if (url) {
printf("\x1B]8;;%s\x1B\\", url);
}
if (FillParagraphWC(s, output) == OK) { if (FillParagraphWC(s, output) == OK) {
if (url) {
printf("\x1B]8;;\x1B\\");
}
return; return;
} }
@@ -1899,6 +1906,9 @@ void FillParagraph(char const *s, DynamicBuffer *output)
continue; continue;
} }
if (!*s) { if (!*s) {
if (url) {
printf("\x1B]8;;\x1B\\");
}
return; return;
} }
/* Over here, we're at the beginning of a line. Emit the correct /* Over here, we're at the beginning of a line. Emit the correct
@@ -1933,6 +1943,9 @@ void FillParagraph(char const *s, DynamicBuffer *output)
len++; len++;
} }
if (s == t) { if (s == t) {
if (url) {
printf("\x1B]8;;\x1B\\");
}
return; return;
} }
if (!pendspace || len+pendspace <= roomleft) { if (!pendspace || len+pendspace <= roomleft) {

View File

@@ -144,7 +144,7 @@ int ParseNonSpaceChar (ParsePtr p, int *err, int peek);
unsigned int HashVal_preservecase(char const *str); unsigned int HashVal_preservecase(char const *str);
int DateOK (int y, int m, int d); int DateOK (int y, int m, int d);
BuiltinFunc *FindBuiltinFunc (char const *name); BuiltinFunc *FindBuiltinFunc (char const *name);
int InsertIntoSortBuffer (int dse, int tim, char const *body, int typ, int prio); int InsertIntoSortBuffer (int dse, int tim, char const *url, char const *body, int typ, int prio);
void IssueSortedReminders (void); void IssueSortedReminders (void);
UserFunc *FindUserFunc(char const *name); UserFunc *FindUserFunc(char const *name);
int UserFuncExists (char const *fn); int UserFuncExists (char const *fn);
@@ -160,7 +160,7 @@ int GetSysVar (char const *name, Value *val);
int SetSysVar (char const *name, Value *val); int SetSysVar (char const *name, Value *val);
void DumpSysVarByName (char const *name); void DumpSysVarByName (char const *name);
int CalcMinsFromUTC (int dse, int tim, int *mins, int *isdst); int CalcMinsFromUTC (int dse, int tim, int *mins, int *isdst);
void FillParagraph (char const *s, DynamicBuffer *output); void FillParagraph (char const *url, char const *s, DynamicBuffer *output);
void LocalToUTC (int locdate, int loctime, int *utcdate, int *utctime); void LocalToUTC (int locdate, int loctime, int *utcdate, int *utctime);
void UTCToLocal (int utcdate, int utctime, int *locdate, int *loctime); void UTCToLocal (int utcdate, int utctime, int *locdate, int *loctime);
int MoonPhase (int date, int time); int MoonPhase (int date, int time);

View File

@@ -140,7 +140,7 @@ int QueueReminder(ParsePtr p, Trigger *trig,
TimeTrig const *tim, char const *sched) TimeTrig const *tim, char const *sched)
{ {
QueuedRem *qelem; QueuedRem *qelem;
TrigInfo *ti;
if (DontQueue || if (DontQueue ||
trig->noqueue || trig->noqueue ||
tim->ttime == NO_TIME || tim->ttime == NO_TIME ||
@@ -169,8 +169,13 @@ int QueueReminder(ParsePtr p, Trigger *trig,
qelem->tt = *tim; qelem->tt = *tim;
qelem->t = *trig; qelem->t = *trig;
/* Take over infos */ /* Copy infos */
trig->infos = NULL; qelem->t.infos = NULL;
ti = trig->infos;
while(ti) {
(void) AppendTrigInfo(&qelem->t, ti->info);
ti = ti->next;
}
DBufInit(&(qelem->t.tags)); DBufInit(&(qelem->t.tags));
DBufPuts(&(qelem->t.tags), DBufValue(&(trig->tags))); DBufPuts(&(qelem->t.tags), DBufValue(&(trig->tags)));

View File

@@ -25,6 +25,7 @@
typedef struct sortrem { typedef struct sortrem {
struct sortrem *next; struct sortrem *next;
char const *text; char const *text;
char const *url;
int trigdate; int trigdate;
int trigtime; int trigtime;
int typ; int typ;
@@ -34,7 +35,7 @@ typedef struct sortrem {
/* The sorted reminder queue */ /* The sorted reminder queue */
static Sortrem *SortedQueue = (Sortrem *) NULL; static Sortrem *SortedQueue = (Sortrem *) NULL;
static Sortrem *MakeSortRem (int dse, int tim, char const *body, int typ, int prio); static Sortrem *MakeSortRem (int dse, int tim, char const *url, char const *body, int typ, int prio);
static void IssueSortBanner (int dse); static void IssueSortBanner (int dse);
/***************************************************************/ /***************************************************************/
@@ -44,23 +45,33 @@ static void IssueSortBanner (int dse);
/* Create a new Sortrem entry - return NULL on failure. */ /* Create a new Sortrem entry - return NULL on failure. */
/* */ /* */
/***************************************************************/ /***************************************************************/
static Sortrem *MakeSortRem(int dse, int tim, char const *body, int typ, int prio) static Sortrem *MakeSortRem(int dse, int tim, char const *url, char const *body, int typ, int prio)
{ {
Sortrem *new = NEW(Sortrem); Sortrem *srem = NEW(Sortrem);
if (!new) return NULL; if (!srem) return NULL;
new->text = strdup(body); srem->text = strdup(body);
if (!new->text) { if (!srem->text) {
free(new); free(srem);
return NULL; return NULL;
} }
if (url) {
new->trigdate = dse; srem->url = strdup(url);
new->trigtime = tim; if (!srem->url) {
new->typ = typ; free((char *) srem->text);
new->priority = prio; free(srem);
new->next = NULL; return NULL;
return new; }
} else {
srem->url = NULL;
}
srem->trigdate = dse;
srem->trigtime = tim;
srem->typ = typ;
srem->priority = prio;
srem->next = NULL;
return srem;
} }
/***************************************************************/ /***************************************************************/
@@ -70,13 +81,13 @@ static Sortrem *MakeSortRem(int dse, int tim, char const *body, int typ, int pri
/* Insert a reminder into the sort buffer */ /* Insert a reminder into the sort buffer */
/* */ /* */
/***************************************************************/ /***************************************************************/
int InsertIntoSortBuffer(int dse, int tim, char const *body, int typ, int prio) int InsertIntoSortBuffer(int dse, int tim, char const *url, char const *body, int typ, int prio)
{ {
Sortrem *new = MakeSortRem(dse, tim, body, typ, prio); Sortrem *srem = MakeSortRem(dse, tim, url, body, typ, prio);
Sortrem *cur = SortedQueue, *prev = NULL; Sortrem *cur = SortedQueue, *prev = NULL;
int ShouldGoAfter; int ShouldGoAfter;
if (!new) { if (!srem) {
Eprint("%s", GetErr(E_NO_MEM)); Eprint("%s", GetErr(E_NO_MEM));
IssueSortedReminders(); IssueSortedReminders();
SortByDate = 0; SortByDate = 0;
@@ -88,11 +99,11 @@ int InsertIntoSortBuffer(int dse, int tim, char const *body, int typ, int prio)
/* Find the correct place in the sorted list */ /* Find the correct place in the sorted list */
if (!SortedQueue) { if (!SortedQueue) {
SortedQueue = new; SortedQueue = srem;
return OK; return OK;
} }
while (cur) { while (cur) {
ShouldGoAfter = CompareRems(new->trigdate, new->trigtime, new->priority, ShouldGoAfter = CompareRems(srem->trigdate, srem->trigtime, srem->priority,
cur->trigdate, cur->trigtime, cur->priority, cur->trigdate, cur->trigtime, cur->priority,
SortByDate, SortByTime, SortByPrio, UntimedBeforeTimed); SortByDate, SortByTime, SortByPrio, UntimedBeforeTimed);
@@ -101,22 +112,21 @@ int InsertIntoSortBuffer(int dse, int tim, char const *body, int typ, int prio)
cur = cur->next; cur = cur->next;
} else { } else {
if (prev) { if (prev) {
prev->next = new; prev->next = srem;
new->next = cur; srem->next = cur;
} else { } else {
SortedQueue = new; SortedQueue = srem;
new->next = cur; srem->next = cur;
} }
return OK; return OK;
} }
} }
prev->next = new; prev->next = srem;
new->next = cur; /* For safety - actually redundant */ srem->next = cur; /* For safety - actually redundant */
return OK; return OK;
} }
/***************************************************************/ /***************************************************************/
/* */ /* */
/* IssueSortedReminders */ /* IssueSortedReminders */
@@ -141,7 +151,13 @@ void IssueSortedReminders(void)
IssueSortBanner(cur->trigdate); IssueSortBanner(cur->trigdate);
olddate = cur->trigdate; olddate = cur->trigdate;
} }
if (cur->url) {
printf("\x1B]8;;%s\x1B\\", cur->url);
}
printf("%s", cur->text); printf("%s", cur->text);
if (cur->url) {
printf("\x1B]8;;\x1B\\");
}
} }
break; break;
@@ -150,7 +166,7 @@ void IssueSortedReminders(void)
IssueSortBanner(cur->trigdate); IssueSortBanner(cur->trigdate);
olddate = cur->trigdate; olddate = cur->trigdate;
} }
FillParagraph(cur->text, NULL); FillParagraph(cur->url, cur->text, NULL);
break; break;
case RUN_TYPE: case RUN_TYPE:
@@ -159,6 +175,9 @@ void IssueSortedReminders(void)
} }
free((char *) cur->text); free((char *) cur->text);
if (cur->url) {
free((char *) cur->url);
}
free(cur); free(cur);
cur = next; cur = next;
} }

View File

@@ -1109,6 +1109,7 @@ static SysVar SysVarArr[] = {
{"Tb", 0, SPECIAL_TYPE, trig_base_func, 0, 0 }, {"Tb", 0, SPECIAL_TYPE, trig_base_func, 0, 0 },
{"Td", 0, SPECIAL_TYPE, trig_day_func, 0, 0 }, {"Td", 0, SPECIAL_TYPE, trig_day_func, 0, 0 },
{"TerminalBackground", 0, SPECIAL_TYPE, terminal_bg_func, 0, 0 }, {"TerminalBackground", 0, SPECIAL_TYPE, terminal_bg_func, 0, 0 },
{"TerminalHyperlinks", 1, INT_TYPE, &TerminalHyperlinks, 0, 1 },
{"Thursday", 1, TRANS_TYPE, "Thursday", 0, 0 }, {"Thursday", 1, TRANS_TYPE, "Thursday", 0, 0 },
{"TimeSep", 1, SPECIAL_TYPE, time_sep_func, 0, 0 }, {"TimeSep", 1, SPECIAL_TYPE, time_sep_func, 0, 0 },
{"TimetIs64bit", 0, SPECIAL_TYPE, timet_is_64_func, 0, 0 }, {"TimetIs64bit", 0, SPECIAL_TYPE, timet_is_64_func, 0, 0 },

View File

@@ -402,6 +402,7 @@ Tcl
Td Td
Terbeck Terbeck
TerminalBackground TerminalBackground
TerminalHyperlinks
Thronicke Thronicke
TimeSep TimeSep
TimetIs64bit TimetIs64bit

View File

@@ -961,6 +961,14 @@ SET $ParseUntriggered 1
REM 1 Jan 1994 MSG 1/0 = [1/0] REM 1 Jan 1994 MSG 1/0 = [1/0]
EOF EOF
# Calendars with hyperlinks
$REMIND -w,0,0 -@2 -c - 1 Jan 2020 <<'EOF' >> $OUT 2>&1
SET $TerminalHyperlinks 1
REM 15 INFO "Url: https://dianne.skoll.ca" MSG Hello, linky!
REM 16 INFO "Url: https://dianne.skoll.ca" MSF Hello, linky!
REM 17 INFO "Url: https://dianne.skoll.ca" CAL Hello, linky!
REM 18 INFO "Url: https://dianne.skoll.ca" SPECIAL COLOR 255 0 0 Hello, linky!
EOF
cmp -s $OUT $CMP cmp -s $OUT $CMP
if [ "$?" = "0" ]; then if [ "$?" = "0" ]; then
echo "Remind: Acceptance tests ${GRN}PASSED${NRM}" echo "Remind: Acceptance tests ${GRN}PASSED${NRM}"

View File

@@ -16759,7 +16759,10 @@ mbpad("
bad => "ÿ" bad => "ÿ"
mbpad("ÿ", "bar", 8, 1) => Invalid multibyte sequence mbpad("ÿ", "bar", 8, 1) => Invalid multibyte sequence
../tests/test.rem(1891): mbpad(): Invalid multibyte sequence ../tests/test.rem(1891): mbpad(): Invalid multibyte sequence
DynBuf Mallocs: 1114 mallocs; 31872128 bytes ]8;;https://dianne.skoll.ca\Hello, linky!
]8;;\]8;;https://dianne.skoll.ca\Hello, linky!
]8;;\]8;;https://dianne.skoll.ca\Hello, linky!
]8;;\DynBuf Mallocs: 1120 mallocs; 31872640 bytes
Variable hash table statistics: Variable hash table statistics:
Entries: 100146; Buckets: 87719; Non-empty Buckets: 66303 Entries: 100146; Buckets: 87719; Non-empty Buckets: 66303
Maxlen: 5; Minlen: 0; Avglen: 1.142; Stddev: 0.878; Avg nonempty len: 1.510 Maxlen: 5; Minlen: 0; Avglen: 1.142; Stddev: 0.878; Avg nonempty len: 1.510
@@ -16781,7 +16784,7 @@ Expression nodes high-water: 302076
Expression nodes leaked: 0 Expression nodes leaked: 0
Parse level high-water: 34 Parse level high-water: 34
Max expr node evaluations per line: 2001 Max expr node evaluations per line: 2001
Total expression node evaluations: 106732 Total expression node evaluations: 106733
Test 2 Test 2
@@ -24950,6 +24953,7 @@ $T
$Tb $Tb
$Td $Td
$TerminalBackground $TerminalBackground
$TerminalHyperlinks
$Thursday $Thursday
$TimeSep $TimeSep
$TimetIs64bit $TimetIs64bit
@@ -40154,3 +40158,21 @@ February 28
-stdin-(2): `/': Division by zero -stdin-(2): `/': Division by zero
-stdin-(2): `/': Division by zero -stdin-(2): `/': Division by zero
# rem2ps end # rem2ps end
+----------------------------------------------------------------------------+
| January 2020 |
+----------+----------+----------+----------+----------+----------+----------+
| Sunday | Monday | Tuesday |Wednesday | Thursday | Friday | Saturday |
+----------+----------+----------+----------+----------+----------+----------+
| | | |1 |2 |3 |4 |
+----------+----------+----------+----------+----------+----------+----------+
|5 |6 |7 |8 |9 |10 |11 |
+----------+----------+----------+----------+----------+----------+----------+
|12 |13 |14 |15 |16 |17 |18 |
| | | |]8;;https://dianne.skoll.ca\Hello,]8;;\ |]8;;https://dianne.skoll.ca\Hello,]8;;\ |]8;;https://dianne.skoll.ca\Hello,]8;;\ |]8;;https://dianne.skoll.ca\Hello,]8;;\ |
| | | |]8;;https://dianne.skoll.ca\linky!]8;;\ |]8;;https://dianne.skoll.ca\linky!]8;;\ |]8;;https://dianne.skoll.ca\linky!]8;;\ |]8;;https://dianne.skoll.ca\linky!]8;;\ |
+----------+----------+----------+----------+----------+----------+----------+
|19 |20 |21 |22 |23 |24 |25 |
+----------+----------+----------+----------+----------+----------+----------+
|26 |27 |28 |29 |30 |31 | |
+----------+----------+----------+----------+----------+----------+----------+

View File

@@ -1891,6 +1891,11 @@ set a mbpad(bad, "bar", 8)
set a mbpad(bad, "bar", 8, 1) set a mbpad(bad, "bar", 8, 1)
DEBUG -x DEBUG -x
SET $TerminalHyperlinks 1
REM INFO "Url: https://dianne.skoll.ca" MSG Hello, linky!
REM INFO "Url: https://dianne.skoll.ca" MSF Hello, linky!
REM INFO "Url: https://dianne.skoll.ca" SPECIAL COLOR 255 0 0 Hello, linky!
# Don't want Remind to queue reminders # Don't want Remind to queue reminders
EXIT EXIT