sdep

A simple "date+event" line parser
git clone https://git.tronto.net/sdep
Download | Log | Files | Refs | README | LICENSE

commit 82bcfe3f25b1f0c08c93fd35497019b546e24ce2
parent 61d15dd4854fe4607442b87602448d6dee16b6a8
Author: Sebastiano Tronto <sebastiano.tronto@gmail.com>
Date:   Sat,  8 May 2021 19:19:41 +0200

First commit, v0.1

Diffstat:
AMakefile | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MREADME.md | 95++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Ascripts/sdep-add | 24++++++++++++++++++++++++
Ascripts/sdep-checknow | 37+++++++++++++++++++++++++++++++++++++
Ascripts/sdep-clear | 11+++++++++++
Ascripts/sdep-edit | 11+++++++++++
Ascripts/sdep-list | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asdep.1 | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asdep.c | 250+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 655 insertions(+), 1 deletion(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,57 @@ +# See LICENSE file for copyright and license details. + +VERSION = 0.1 + +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man +SDEPDATA = ${XDG_DATA_HOME}/sdep +SCRIPTS = sdep-add sdep-checknow sdep-clear sdep-edit sdep-list + +CPPFLAGS = -D_XOPEN_SOURCE=700 -DVERSION=\"${VERSION}\" +CFLAGS = -pedantic -Wall -Os ${CPPFLAGS} + +CC = cc + + +all: options sdep + +options: + @echo sdep build options: + @echo "CFLAGS = ${CFLAGS}" + @echo "CC = ${CC}" + +sdep: + ${CC} ${CFLAGS} -o sdep sdep.c + +clean: + rm -f sdep sdep-${VERSION}.tar.gz + +dist: clean + mkdir -p sdep-${VERSION} + cp -R LICENSE Makefile README sdep.1 sdep.c sdep-${VERSION} + tar -cf sdep-${VERSION}.tar sdep-${VERSION} + gzip sdep-${VERSION}.tar + rm -rf sdep-${VERSION} + +install: all + mkdir -p ${DESTDIR}${PREFIX}/bin + cp -f sdep ${DESTDIR}${PREFIX}/bin + chmod 755 ${DESTDIR}${PREFIX}/bin/sdep + mkdir -p ${DESTDIR}${MANPREFIX}/man1 + sed "s/VERSION/${VERSION}/g" < sdep.1 > ${DESTDIR}${MANPREFIX}/man1/sdep.1 + chmod 644 ${DESTDIR}${MANPREFIX}/man1/sdep.1 + +scripts: + mkdir -p ${DESTDIR}${SDEPDATA} + for s in ${SCRIPTS}; do\ + sed "s|SDEPDATA|${DESTDIR}${SDEPDATA}|g" < scripts/$$s \ + > ${DESTDIR}${PREFIX}/bin/$$s ;\ + chmod 755 ${DESTDIR}${PREFIX}/bin/$$s ;\ + done + +uninstall: + rm -rf ${DESTDIR}${PREFIX}/bin/sdep ${DESTDIR}${MANPREFIX}/man1/sdep.1 + for s in ${SCRIPTS}; do rm -rf ${DESTDIR}${PREFIX}/bin/$$s; done + +.PHONY: all options clean dist install scripts uninstall + diff --git a/README.md b/README.md @@ -1,2 +1,95 @@ # sdep -A simple "date+event" line parser +A simple "date+event" line parser. + +sdep follows the UNIX philosphy (do one thing well, use stdin and stdout) and +is heavily inspired by [suckless](https://suckless.org) utilities such as +[dmenu](https://tools.suckless.org/dmenu/). + +You can wrap it around shell scripts to turn it into a no-nonsense calendar +system. + +## Description + +sdep reads lines of the form `date text` from stdin and writes to stdout those +lines such that `date` is between the two dates specified by the `-f` and `-t` +options, both of which default to the current minute. + +The dates should correspond to a unique minute in time. The format for `date` +can be specified with the same syntax as for date(1). + +## Installation +Edit the Makefile to match your local configuration and type `make install`. + +## Examples +If `events.txt` contains lines formatted as `date text` then + +``` +sdep <events.txt +``` + +will print all lines whose date match the current minute. Instead + +``` +sdep -f <events.txt +``` + +will print all the lines whose date is in the past, while + +``` +sdep -t -w "%A" <events.txt +``` + +will print all lines whose date is in the future, showing only the day of the +week and the text. + +``` +sdep -f "1999-01-01 00:00" -t "1999-12-31 23:59" -w "" <events.txt +``` + +will show only the `text` of all lines with a date in 1999. You can specify a +different format for the dates, for example + +``` +sdep +"%m/%d/%Y %I:%M%p" -t "12/31/2020 11:59pm" -w "" <events.txt +``` + +will match all dates from December 31st, 2020, one minute before midnight +(included). Note: this only works if your locale has an am/pm format, see +date(1). + +## A stupidly simple calendar app +If you keep your events and reminders in a simple plain text file (say +events.txt), you can run + +``` +sdep -w "" -s "" | while read text; do notify-send "$text"; done < events.txt +``` + +every minute, for example using cron(8), to get a notification every time +an events is happens. + +You can use `sdep -f -t < events.txt` to list all your events, or +`sdep -t < events.txt` to list only the future ones. You can specify any date +range. Running + +``` +temp = $(mktemp) +sdep -t <events.txt >"$temp" +mv "$temp" events.txt +``` + +will remove all old events from your file. + +You can edit your events using any text editor and you can keep them synced +between multiple devices using something like +[rsync](https://rsync.samba.org/). + +## Scripts +The `scripts` folder contains the few scripts that I use. They are basically +just a slightly more elaborate version of the calendar system described above, +with support for recurring events (e.g. weekly, daily). You can install them +with `make scripts`, but first make sure to adjust them to match your local +configuration, like the location of the events file. + +Most of the script rely on the `-d` option of the GNU date utility, so you +should change that too if you are on a BSD system or on MacOS. diff --git a/scripts/sdep-add b/scripts/sdep-add @@ -0,0 +1,24 @@ +#!/bin/sh + +# Adds a line to SDEPDATA/once. It is possible to specify a date, which will be +# read by date -d and prepended to the input text. +# -d is a non-portable option of GNU date. On some other systems one can +# accomplish the same with the -j option. + +# Usage: sdep-add [date] +# Example: sdep-add "next monday" + +file="SDEPDATA/once" +date="" + +[ -n "$1" ] && date=$(date -d "$1" +%Y-%m-%d) + +[ -n "$date" ] && printf "$date " + +read text + +if [ -n "$text" ]; then + printf "${date}\t${text}\n" >> "$file" +else + echo "Event not saved" +fi diff --git a/scripts/sdep-checknow b/scripts/sdep-checknow @@ -0,0 +1,37 @@ +#!/bin/sh + +# Run this every minute (for example using cron) to receive a notification +# when an event happens. +# +# The events are read from the following files in SDEPDATA folder: +# once: events that only happen once; full date needs to be specified +# daily: events that happen every day; only specify the time in HH:MM format +# weekly: only specify Weekday + time +# monthly: only specify day of the month + time + +# Requires: notify-send; the weekly part uses the non-portable -d option of +# GNU date, which you might have to change if you are not on Linux (on some +# systems you can accomplish the same with -j). + +notify="notify-send -t 3600000" +title="$(date +%H:%M)" + +sdepnotify() { + sdep -w "" -s "" | while read text; do $notify "$title" "$text"; done +} + +# One-off events +sdepnotify <"SDEPDATA/once" + +# Daily events +sed "s/^/$(date +%Y-%m-%d) /" <"SDEPDATA/daily" | sdepnotify + +# Weekly events - needs GNU date (try -j intead of -d on BSD) +while read line; do + weekday=$(echo "$line" | sed "s/ .*$//") + echo "$line" | sed "s/^[ ]*[^ ]* /$(date -d $weekday +%Y-%m-%d) /" +done <"SDEPDATA/weekly" | \ +sdepnotify + +# Monthly events +sed "s/^[ ]*/$(date +%Y-%m-)/" <"SDEPDATA/daily" | sdepnotify diff --git a/scripts/sdep-clear b/scripts/sdep-clear @@ -0,0 +1,11 @@ +#!/bin/sh + +# Removes old events from specified file (default: SDEPDATA/once). + +# Usage: sdep-clear [file] + +file="SDEPDATA/${1:-once}" +temp=$(mktemp) + +sdep -t <"$file" >"$temp" +mv "$temp" "$file" diff --git a/scripts/sdep-edit b/scripts/sdep-edit @@ -0,0 +1,11 @@ +#!/bin/sh + +# Open specified file in sdep folder in editor. + +# Usage: sdep-edit [file] +# No options = once + +file="SDEPFOLDER/${1:-once}" +editor=${VISUAL:-vi} + +$editor "$file" diff --git a/scripts/sdep-list b/scripts/sdep-list @@ -0,0 +1,65 @@ +#!/bin/sh + +# Lists events happening in a specific time range. +# Recurring events are listed at most once, and only if their first occurrence +# is in the range. +# +# The events are read from the following files in the SDEPDATA folder: +# once: events that only happen once; full date needs to be specified +# daily: events that happen every day; only specify the time in HH:MM format +# weekly: only specify Weekday + time +# monthly: only specify day of the month + time + +# This script uses relies on the non-portable -d option of GNU date. + +# Usage: sdep-list [today|tomorrow|week|nextweek|past|future] +# No options = all + +from="" +to="" +w="%d %b %H:%M" + +case $1 in + today) + from="$(date +%Y-%m-%d) 0:00" + to="$(date +%Y-%m-%d) 23:59" + w="%H:%M" + ;; + tomorrow) + from="$(date -d tomorrow +%Y-%m-%d) 0:00" + to="$(date -d tomorrow +%Y-%m-%d) 23:59" + w="%H:%M" + ;; + week) + from="$(date -d 'last monday' +%Y-%m-%d) 0:00" + to="$(date -d sunday +%Y-%m-%d) 23:59" + w="%a %H:%M" + ;; + nextweek) + from="$(date -d 'next monday' +%Y-%m-%d) 0:00" + to="$(date -d 'next monday + 6 days' +%Y-%m-%d) 23:59" + w="%a %H:%M" + ;; + past) + to="$(date +'%Y-%m-%d %H:%M')" + ;; + future) + from="$(date +'%Y-%m-%d %H:%M')" + ;; + *) + ;; +esac + +tempfile=$(mktemp) + +cat "SDEPDATA/once" > "$tempfile" +sed "s/^/$(date +%Y-%m-%d) /" <"SDEPDATA/daily" | >> "$tempfile" +while read line; do + weekday=$(echo "$line" | sed "s/ .*$//") + echo "$line" | sed "s/^[ ]*[^ ]* /$(date -d $weekday +%Y-%m-%d) /" +done <"SDEPDATA/weekly" >> "$tempfile" +sed "s/^[ ]*/$(date +%Y-%m-)/" <"SDEPDATA/daily" >> "$tempfile" + +sdep -w "$w" -f "$from" -t "$to" <"$tempfile" + +rm "$tempfile" diff --git a/sdep.1 b/sdep.1 @@ -0,0 +1,106 @@ +.TH SDEP 1 sdep\-VERSION + +.SH NAME +sdep \- a simple "date+event" line parser + +.SH SYNOPSIS +.B sdep +.RB [ \-dv ] +.IR "[+format]" +.RB [ \-f +.IR "[date]" ] +.RB [ \-t +.IR "[date]" ] +.RB [ \-w +.IR format ] +.RB [ \-s +.IR string ] + +.SH DESCRIPTION +.B sdep +reads lines of the form + +.IR "date text" + +from stdin and writes to stdout those lines such that +.IR date +is between the two dates specified by the +.IR \-f +and +.IR \-t +options, both of which default to the current minute. +The dates should correspond to a unique minute in time. The format for +.IR date +can be specified with the same syntax as for +.IR date (1). + +.SH OPTIONS + +.TP +.B \-d +print the default date format and exit. + +.TP +.BI \-f " [date]" +initial date for the range (default: current minute). If +.I date +is not specified then there will be no lower bound for the dates. + +.TP +.BI \-s " string" +change the string that separates the date from the text in the output +lines (deafult: "\t"). + +.TP +.BI \-t " [date]" +final date for the range (default: current minute). If +.I date +is not specified then there will be no upper bound for the dates. + +.TP +.B \-v +print version information and exit. + +.TP +.BI \-w " format" +change the format in which the date is written in the output lines. + +.SH EXAMPLES +If +.I events.txt +contains lines formatted as +.I "date text" +then + +sdep -f <events.txt + +will print all the lines whose date is in the past, while + +sdep -t -w "%A" <events.txt + +will print all lines whose date is in the future, showing only the day of the +week and the text. + +sdep -f "1999-01-01 00:00" -t "1999-12-31 23:59" -w "" <events.txt + +will show only the +.I text +of all lines with a date in 1999. You can specify a different format for the +dates, for example + +sdep +"%m/%d/%Y %I:%M%p" -t "12/31/2020 11:59pm" -w "" <events.txt + +will match all dates from December 31st, 2020, one minute before midnight +(included). Note: this only works if your locale has an am/pm format, see +.IR date (1). + +.SH AUTHORS +Sebastiano Tronto <sebastiano.tronto@gmail.com> + +.SH SOURCE CODE +Source code is available at https://github.com/sebastianotronto/sdep + +.SH SEE ALSO +.IR date (1), +.IR strftime (3), +.IR strptime (3) diff --git a/sdep.c b/sdep.c @@ -0,0 +1,250 @@ +/* See LICENSE file for copyright and license details. */ + +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#define min(X,Y) (X<Y ? X : Y) + +/* + * Maximum number of characters in a line. The rest will be truncated. + * Change this if you need very long lines. + */ +static const int MAXLEN = 10000; + +/* + * Default date format. Anything that strftime(3) understands works, but + * it should determine a date completely up to the minute. + */ +static char *default_format = "%Y-%m-%d %H:%M"; + +typedef struct Event Event; +typedef struct EventList EventList; +typedef struct EventNode EventNode; +typedef struct Options Options; + +struct Event{ + struct tm time; + char *text; +}; + +struct EventList { + EventNode *first; + EventNode *last; + int count; +}; + +struct EventNode { + Event ev; + EventNode *next; +}; + +struct Options { + struct tm from; + struct tm to; + char *format_in; + char *format_out; + char *separator; +}; + +static void add_event(struct tm t, char *text, EventList *evlist); +static int compare_tm(struct tm *t1, struct tm *t2); +static int compare_event(Event *event1, Event *event2); +static Options default_op(); +static int events_in_range(EventList *evlist, Options op, Event *sel); +static char *format_line(Event ev, Options op, char *out); +static void read_input(Options op, EventList *evlist); +static Options read_op(int argc, char *argv[]); +static char *strtrim(char *t); +static void write_output(Options op, Event *ev, int n); + + +static void +add_event(struct tm t, char *text, EventList *evlist) +{ + int l = strlen(text)+1; + EventNode *next = malloc(sizeof(EventNode)); + + next->ev.time = t; + next->ev.text = malloc(sizeof(char) * l); + strncpy(next->ev.text, text, l); + next->ev.text = strtrim(next->ev.text); + next->next = NULL; + + if (++evlist->count == 1) { + evlist->first = next; + evlist->last = next; + } else { + evlist->last->next = next; + evlist->last = next; + } +} + +static int +compare_tm(struct tm *t1, struct tm *t2) +{ + if (t1->tm_year != t2->tm_year) + return t1->tm_year - t2->tm_year; + if (t1->tm_mon != t2->tm_mon) + return t1->tm_mon - t2->tm_mon; + if (t1->tm_mday != t2->tm_mday) + return t1->tm_mday - t2->tm_mday; + if (t1->tm_hour != t2->tm_hour) + return t1->tm_hour - t2->tm_hour; + return t1->tm_min - t2->tm_min; +} + +static int +compare_event(Event *ev1, Event *ev2) +{ + return compare_tm(&ev1->time, &ev2->time); +} + +static Options +default_op() +{ + Options op; + time_t t_now = time(NULL); + struct tm *now = localtime(&t_now); + + op.format_in = malloc(sizeof(char) * MAXLEN); + op.format_out = malloc(sizeof(char) * MAXLEN); + op.separator = malloc(sizeof(char) * MAXLEN); + strcpy(op.format_in, default_format); + strcpy(op.format_out, default_format); + strcpy(op.separator, "\t"); + op.from = *now; + op.to = *now; + + return op; +} + +/* +* Saves the events in ev[] that happen between op->from and op->to in sel[] +* sorted by date and returns their number. +*/ +static int +events_in_range(EventList *evlist, Options op, Event *sel) +{ + EventNode *i; + int n = 0; + + + for (i = evlist->first; i != NULL; i = i->next) + if (compare_tm(&i->ev.time, &op.from) >= 0 && + compare_tm(&i->ev.time, &op.to) <= 0) + sel[n++] = i->ev; + + qsort(sel, n, sizeof(Event), + (int (*)(const void *, const void *))compare_event); + + return n; +} + +static char * +format_line(Event ev, Options op, char *out) +{ + strftime(out, MAXLEN, op.format_out, &ev.time); + strncat(out, op.separator, MAXLEN - strlen(out)); + strncat(out, ev.text, MAXLEN - strlen(out)); + + return out; +} + +static void +read_input(Options op, EventList *evlist) +{ + struct tm t; + char line[MAXLEN], *text_ptr; + + while (fgets(line, MAXLEN, stdin) != NULL) + if ((text_ptr = strptime(line, op.format_in, &t)) != NULL) + add_event(t, text_ptr, evlist); +} + +static Options +read_op(int argc, char *argv[]) +{ + Options op = default_op(); + int i; + + /* Check for format specification. + * This changes the way other options are read */ + for (i = 1; i < argc; i++) { + if (argv[i][0] == '+') { + strncpy(op.format_in, &argv[i][1], MAXLEN); + strncpy(op.format_out, &argv[i][1], MAXLEN); + } + } + + for (i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-v")) { + puts("sdep-"VERSION); + exit(0); + } else if (!strcmp(argv[i], "-d")) { + puts(default_format); + exit(0); + } else if (!strcmp(argv[i], "-s")) { + strncpy(op.separator, argv[++i], MAXLEN); + } else if (!strcmp(argv[i], "-f")) { + if (i+1 >= argc || + strptime(argv[i+1], op.format_in, &op.from) == NULL) + op.from.tm_year = -1000000000; /* Very large number */ + else + i++; + } else if (!strcmp(argv[i], "-t")) { + if (i+1 >= argc || + strptime(argv[i+1], op.format_in, &op.to) == NULL) + op.to.tm_year = 1000000000; /* Very small number */ + else + i++; + } else if (!strcmp(argv[i], "-w")) { + op.format_out = argv[++i]; + } else if (argv[i][0] != '+' && strlen(argv[i]) != 0) { + fprintf(stderr, "usage: sdep [-dv]"); + fprintf(stderr, " [+format] [-f [date]] [-t [date]]"); + fprintf(stderr, " [-w format] [-s string]\n"); + exit(1); + } + } + + return op; +} + +static char * +strtrim(char *t) +{ + char *s; + + for (s = &t[strlen(t)-1]; s != t && isspace(*s); *s = '\0', s--); + for (; *t != '\0' && isspace(*t); t++); + + return t; +} + +static void +write_output(Options op, Event *ev, int n) +{ + char outline[MAXLEN]; + int i; + + for (i = 0; i < n; i++) + printf("%s\n", format_line(ev[i], op, outline)); +} + +int +main(int argc, char *argv[]) +{ + Options op; + EventList evlist = {0}; + Event *selected; + + op = read_op(argc, argv); + read_input(op, &evlist); + selected = malloc(sizeof(Event) * evlist.count); + write_output(op, selected, events_in_range(&evlist, op, selected)); + + return 0; +}