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:
A | Makefile | | | 57 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | README.md | | | 95 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
A | scripts/sdep-add | | | 24 | ++++++++++++++++++++++++ |
A | scripts/sdep-checknow | | | 37 | +++++++++++++++++++++++++++++++++++++ |
A | scripts/sdep-clear | | | 11 | +++++++++++ |
A | scripts/sdep-edit | | | 11 | +++++++++++ |
A | scripts/sdep-list | | | 65 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | sdep.1 | | | 106 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | sdep.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;
+}