aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastiano Tronto <sebastiano.tronto@gmail.com>2021-05-08 19:19:41 +0200
committerSebastiano Tronto <sebastiano.tronto@gmail.com>2021-05-08 19:19:41 +0200
commit82bcfe3f25b1f0c08c93fd35497019b546e24ce2 (patch)
tree055309330ff16f6d3f754b2ef6eaff92fe3925ed
parent61d15dd4854fe4607442b87602448d6dee16b6a8 (diff)
downloadsdep-82bcfe3f25b1f0c08c93fd35497019b546e24ce2.tar.gz
sdep-82bcfe3f25b1f0c08c93fd35497019b546e24ce2.zip
First commit, v0.1
-rw-r--r--Makefile57
-rw-r--r--README.md95
-rwxr-xr-xscripts/sdep-add24
-rwxr-xr-xscripts/sdep-checknow37
-rwxr-xr-xscripts/sdep-clear11
-rwxr-xr-xscripts/sdep-edit11
-rwxr-xr-xscripts/sdep-list65
-rw-r--r--sdep.1106
-rw-r--r--sdep.c250
9 files changed, 655 insertions, 1 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..2b44f75
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,57 @@
1# See LICENSE file for copyright and license details.
2
3VERSION = 0.1
4
5PREFIX = /usr/local
6MANPREFIX = ${PREFIX}/share/man
7SDEPDATA = ${XDG_DATA_HOME}/sdep
8SCRIPTS = sdep-add sdep-checknow sdep-clear sdep-edit sdep-list
9
10CPPFLAGS = -D_XOPEN_SOURCE=700 -DVERSION=\"${VERSION}\"
11CFLAGS = -pedantic -Wall -Os ${CPPFLAGS}
12
13CC = cc
14
15
16all: options sdep
17
18options:
19 @echo sdep build options:
20 @echo "CFLAGS = ${CFLAGS}"
21 @echo "CC = ${CC}"
22
23sdep:
24 ${CC} ${CFLAGS} -o sdep sdep.c
25
26clean:
27 rm -f sdep sdep-${VERSION}.tar.gz
28
29dist: clean
30 mkdir -p sdep-${VERSION}
31 cp -R LICENSE Makefile README sdep.1 sdep.c sdep-${VERSION}
32 tar -cf sdep-${VERSION}.tar sdep-${VERSION}
33 gzip sdep-${VERSION}.tar
34 rm -rf sdep-${VERSION}
35
36install: all
37 mkdir -p ${DESTDIR}${PREFIX}/bin
38 cp -f sdep ${DESTDIR}${PREFIX}/bin
39 chmod 755 ${DESTDIR}${PREFIX}/bin/sdep
40 mkdir -p ${DESTDIR}${MANPREFIX}/man1
41 sed "s/VERSION/${VERSION}/g" < sdep.1 > ${DESTDIR}${MANPREFIX}/man1/sdep.1
42 chmod 644 ${DESTDIR}${MANPREFIX}/man1/sdep.1
43
44scripts:
45 mkdir -p ${DESTDIR}${SDEPDATA}
46 for s in ${SCRIPTS}; do\
47 sed "s|SDEPDATA|${DESTDIR}${SDEPDATA}|g" < scripts/$$s \
48 > ${DESTDIR}${PREFIX}/bin/$$s ;\
49 chmod 755 ${DESTDIR}${PREFIX}/bin/$$s ;\
50 done
51
52uninstall:
53 rm -rf ${DESTDIR}${PREFIX}/bin/sdep ${DESTDIR}${MANPREFIX}/man1/sdep.1
54 for s in ${SCRIPTS}; do rm -rf ${DESTDIR}${PREFIX}/bin/$$s; done
55
56.PHONY: all options clean dist install scripts uninstall
57
diff --git a/README.md b/README.md
index 5b38847..523ac37 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,95 @@
1# sdep 1# sdep
2A simple "date+event" line parser 2A simple "date+event" line parser.
3
4sdep follows the UNIX philosphy (do one thing well, use stdin and stdout) and
5is heavily inspired by [suckless](https://suckless.org) utilities such as
6[dmenu](https://tools.suckless.org/dmenu/).
7
8You can wrap it around shell scripts to turn it into a no-nonsense calendar
9system.
10
11## Description
12
13sdep reads lines of the form `date text` from stdin and writes to stdout those
14lines such that `date` is between the two dates specified by the `-f` and `-t`
15options, both of which default to the current minute.
16
17The dates should correspond to a unique minute in time. The format for `date`
18can be specified with the same syntax as for date(1).
19
20## Installation
21Edit the Makefile to match your local configuration and type `make install`.
22
23## Examples
24If `events.txt` contains lines formatted as `date text` then
25
26```
27sdep <events.txt
28```
29
30will print all lines whose date match the current minute. Instead
31
32```
33sdep -f <events.txt
34```
35
36will print all the lines whose date is in the past, while
37
38```
39sdep -t -w "%A" <events.txt
40```
41
42will print all lines whose date is in the future, showing only the day of the
43week and the text.
44
45```
46sdep -f "1999-01-01 00:00" -t "1999-12-31 23:59" -w "" <events.txt
47```
48
49will show only the `text` of all lines with a date in 1999. You can specify a
50different format for the dates, for example
51
52```
53sdep +"%m/%d/%Y %I:%M%p" -t "12/31/2020 11:59pm" -w "" <events.txt
54```
55
56will match all dates from December 31st, 2020, one minute before midnight
57(included). Note: this only works if your locale has an am/pm format, see
58date(1).
59
60## A stupidly simple calendar app
61If you keep your events and reminders in a simple plain text file (say
62events.txt), you can run
63
64```
65sdep -w "" -s "" | while read text; do notify-send "$text"; done < events.txt
66```
67
68every minute, for example using cron(8), to get a notification every time
69an events is happens.
70
71You can use `sdep -f -t < events.txt` to list all your events, or
72`sdep -t < events.txt` to list only the future ones. You can specify any date
73range. Running
74
75```
76temp = $(mktemp)
77sdep -t <events.txt >"$temp"
78mv "$temp" events.txt
79```
80
81will remove all old events from your file.
82
83You can edit your events using any text editor and you can keep them synced
84between multiple devices using something like
85[rsync](https://rsync.samba.org/).
86
87## Scripts
88The `scripts` folder contains the few scripts that I use. They are basically
89just a slightly more elaborate version of the calendar system described above,
90with support for recurring events (e.g. weekly, daily). You can install them
91with `make scripts`, but first make sure to adjust them to match your local
92configuration, like the location of the events file.
93
94Most of the script rely on the `-d` option of the GNU date utility, so you
95should change that too if you are on a BSD system or on MacOS.
diff --git a/scripts/sdep-add b/scripts/sdep-add
new file mode 100755
index 0000000..bb0b2c8
--- /dev/null
+++ b/scripts/sdep-add
@@ -0,0 +1,24 @@
1#!/bin/sh
2
3# Adds a line to SDEPDATA/once. It is possible to specify a date, which will be
4# read by date -d and prepended to the input text.
5# -d is a non-portable option of GNU date. On some other systems one can
6# accomplish the same with the -j option.
7
8# Usage: sdep-add [date]
9# Example: sdep-add "next monday"
10
11file="SDEPDATA/once"
12date=""
13
14[ -n "$1" ] && date=$(date -d "$1" +%Y-%m-%d)
15
16[ -n "$date" ] && printf "$date "
17
18read text
19
20if [ -n "$text" ]; then
21 printf "${date}\t${text}\n" >> "$file"
22else
23 echo "Event not saved"
24fi
diff --git a/scripts/sdep-checknow b/scripts/sdep-checknow
new file mode 100755
index 0000000..0319b5f
--- /dev/null
+++ b/scripts/sdep-checknow
@@ -0,0 +1,37 @@
1#!/bin/sh
2
3# Run this every minute (for example using cron) to receive a notification
4# when an event happens.
5#
6# The events are read from the following files in SDEPDATA folder:
7# once: events that only happen once; full date needs to be specified
8# daily: events that happen every day; only specify the time in HH:MM format
9# weekly: only specify Weekday + time
10# monthly: only specify day of the month + time
11
12# Requires: notify-send; the weekly part uses the non-portable -d option of
13# GNU date, which you might have to change if you are not on Linux (on some
14# systems you can accomplish the same with -j).
15
16notify="notify-send -t 3600000"
17title="$(date +%H:%M)"
18
19sdepnotify() {
20 sdep -w "" -s "" | while read text; do $notify "$title" "$text"; done
21}
22
23# One-off events
24sdepnotify <"SDEPDATA/once"
25
26# Daily events
27sed "s/^/$(date +%Y-%m-%d) /" <"SDEPDATA/daily" | sdepnotify
28
29# Weekly events - needs GNU date (try -j intead of -d on BSD)
30while read line; do
31 weekday=$(echo "$line" | sed "s/ .*$//")
32 echo "$line" | sed "s/^[ ]*[^ ]* /$(date -d $weekday +%Y-%m-%d) /"
33done <"SDEPDATA/weekly" | \
34sdepnotify
35
36# Monthly events
37sed "s/^[ ]*/$(date +%Y-%m-)/" <"SDEPDATA/daily" | sdepnotify
diff --git a/scripts/sdep-clear b/scripts/sdep-clear
new file mode 100755
index 0000000..814f853
--- /dev/null
+++ b/scripts/sdep-clear
@@ -0,0 +1,11 @@
1#!/bin/sh
2
3# Removes old events from specified file (default: SDEPDATA/once).
4
5# Usage: sdep-clear [file]
6
7file="SDEPDATA/${1:-once}"
8temp=$(mktemp)
9
10sdep -t <"$file" >"$temp"
11mv "$temp" "$file"
diff --git a/scripts/sdep-edit b/scripts/sdep-edit
new file mode 100755
index 0000000..5e8c491
--- /dev/null
+++ b/scripts/sdep-edit
@@ -0,0 +1,11 @@
1#!/bin/sh
2
3# Open specified file in sdep folder in editor.
4
5# Usage: sdep-edit [file]
6# No options = once
7
8file="SDEPFOLDER/${1:-once}"
9editor=${VISUAL:-vi}
10
11$editor "$file"
diff --git a/scripts/sdep-list b/scripts/sdep-list
new file mode 100755
index 0000000..a9e5405
--- /dev/null
+++ b/scripts/sdep-list
@@ -0,0 +1,65 @@
1#!/bin/sh
2
3# Lists events happening in a specific time range.
4# Recurring events are listed at most once, and only if their first occurrence
5# is in the range.
6#
7# The events are read from the following files in the SDEPDATA folder:
8# once: events that only happen once; full date needs to be specified
9# daily: events that happen every day; only specify the time in HH:MM format
10# weekly: only specify Weekday + time
11# monthly: only specify day of the month + time
12
13# This script uses relies on the non-portable -d option of GNU date.
14
15# Usage: sdep-list [today|tomorrow|week|nextweek|past|future]
16# No options = all
17
18from=""
19to=""
20w="%d %b %H:%M"
21
22case $1 in
23 today)
24 from="$(date +%Y-%m-%d) 0:00"
25 to="$(date +%Y-%m-%d) 23:59"
26 w="%H:%M"
27 ;;
28 tomorrow)
29 from="$(date -d tomorrow +%Y-%m-%d) 0:00"
30 to="$(date -d tomorrow +%Y-%m-%d) 23:59"
31 w="%H:%M"
32 ;;
33 week)
34 from="$(date -d 'last monday' +%Y-%m-%d) 0:00"
35 to="$(date -d sunday +%Y-%m-%d) 23:59"
36 w="%a %H:%M"
37 ;;
38 nextweek)
39 from="$(date -d 'next monday' +%Y-%m-%d) 0:00"
40 to="$(date -d 'next monday + 6 days' +%Y-%m-%d) 23:59"
41 w="%a %H:%M"
42 ;;
43 past)
44 to="$(date +'%Y-%m-%d %H:%M')"
45 ;;
46 future)
47 from="$(date +'%Y-%m-%d %H:%M')"
48 ;;
49 *)
50 ;;
51esac
52
53tempfile=$(mktemp)
54
55cat "SDEPDATA/once" > "$tempfile"
56sed "s/^/$(date +%Y-%m-%d) /" <"SDEPDATA/daily" | >> "$tempfile"
57while read line; do
58 weekday=$(echo "$line" | sed "s/ .*$//")
59 echo "$line" | sed "s/^[ ]*[^ ]* /$(date -d $weekday +%Y-%m-%d) /"
60done <"SDEPDATA/weekly" >> "$tempfile"
61sed "s/^[ ]*/$(date +%Y-%m-)/" <"SDEPDATA/daily" >> "$tempfile"
62
63sdep -w "$w" -f "$from" -t "$to" <"$tempfile"
64
65rm "$tempfile"
diff --git a/sdep.1 b/sdep.1
new file mode 100644
index 0000000..8cbdd24
--- /dev/null
+++ b/sdep.1
@@ -0,0 +1,106 @@
1.TH SDEP 1 sdep\-VERSION
2
3.SH NAME
4sdep \- a simple "date+event" line parser
5
6.SH SYNOPSIS
7.B sdep
8.RB [ \-dv ]
9.IR "[+format]"
10.RB [ \-f
11.IR "[date]" ]
12.RB [ \-t
13.IR "[date]" ]
14.RB [ \-w
15.IR format ]
16.RB [ \-s
17.IR string ]
18
19.SH DESCRIPTION
20.B sdep
21reads lines of the form
22
23.IR "date text"
24
25from stdin and writes to stdout those lines such that
26.IR date
27is between the two dates specified by the
28.IR \-f
29and
30.IR \-t
31options, both of which default to the current minute.
32The dates should correspond to a unique minute in time. The format for
33.IR date
34can be specified with the same syntax as for
35.IR date (1).
36
37.SH OPTIONS
38
39.TP
40.B \-d
41print the default date format and exit.
42
43.TP
44.BI \-f " [date]"
45initial date for the range (default: current minute). If
46.I date
47is not specified then there will be no lower bound for the dates.
48
49.TP
50.BI \-s " string"
51change the string that separates the date from the text in the output
52lines (deafult: "\t").
53
54.TP
55.BI \-t " [date]"
56final date for the range (default: current minute). If
57.I date
58is not specified then there will be no upper bound for the dates.
59
60.TP
61.B \-v
62print version information and exit.
63
64.TP
65.BI \-w " format"
66change the format in which the date is written in the output lines.
67
68.SH EXAMPLES
69If
70.I events.txt
71contains lines formatted as
72.I "date text"
73then
74
75sdep -f <events.txt
76
77will print all the lines whose date is in the past, while
78
79sdep -t -w "%A" <events.txt
80
81will print all lines whose date is in the future, showing only the day of the
82week and the text.
83
84sdep -f "1999-01-01 00:00" -t "1999-12-31 23:59" -w "" <events.txt
85
86will show only the
87.I text
88of all lines with a date in 1999. You can specify a different format for the
89dates, for example
90
91sdep +"%m/%d/%Y %I:%M%p" -t "12/31/2020 11:59pm" -w "" <events.txt
92
93will match all dates from December 31st, 2020, one minute before midnight
94(included). Note: this only works if your locale has an am/pm format, see
95.IR date (1).
96
97.SH AUTHORS
98Sebastiano Tronto <sebastiano.tronto@gmail.com>
99
100.SH SOURCE CODE
101Source code is available at https://github.com/sebastianotronto/sdep
102
103.SH SEE ALSO
104.IR date (1),
105.IR strftime (3),
106.IR strptime (3)
diff --git a/sdep.c b/sdep.c
new file mode 100644
index 0000000..e2b9442
--- /dev/null
+++ b/sdep.c
@@ -0,0 +1,250 @@
1/* See LICENSE file for copyright and license details. */
2
3#include <ctype.h>
4#include <stdio.h>
5#include <stdlib.h>
6#include <string.h>
7#include <time.h>
8
9#define min(X,Y) (X<Y ? X : Y)
10
11/*
12 * Maximum number of characters in a line. The rest will be truncated.
13 * Change this if you need very long lines.
14 */
15static const int MAXLEN = 10000;
16
17/*
18 * Default date format. Anything that strftime(3) understands works, but
19 * it should determine a date completely up to the minute.
20 */
21static char *default_format = "%Y-%m-%d %H:%M";
22
23typedef struct Event Event;
24typedef struct EventList EventList;
25typedef struct EventNode EventNode;
26typedef struct Options Options;
27
28struct Event{
29 struct tm time;
30 char *text;
31};
32
33struct EventList {
34 EventNode *first;
35 EventNode *last;
36 int count;
37};
38
39struct EventNode {
40 Event ev;
41 EventNode *next;
42};
43
44struct Options {
45 struct tm from;
46 struct tm to;
47 char *format_in;
48 char *format_out;
49 char *separator;
50};
51
52static void add_event(struct tm t, char *text, EventList *evlist);
53static int compare_tm(struct tm *t1, struct tm *t2);
54static int compare_event(Event *event1, Event *event2);
55static Options default_op();
56static int events_in_range(EventList *evlist, Options op, Event *sel);
57static char *format_line(Event ev, Options op, char *out);
58static void read_input(Options op, EventList *evlist);
59static Options read_op(int argc, char *argv[]);
60static char *strtrim(char *t);
61static void write_output(Options op, Event *ev, int n);
62
63
64static void
65add_event(struct tm t, char *text, EventList *evlist)
66{
67 int l = strlen(text)+1;
68 EventNode *next = malloc(sizeof(EventNode));
69
70 next->ev.time = t;
71 next->ev.text = malloc(sizeof(char) * l);
72 strncpy(next->ev.text, text, l);
73 next->ev.text = strtrim(next->ev.text);
74 next->next = NULL;
75
76 if (++evlist->count == 1) {
77 evlist->first = next;
78 evlist->last = next;
79 } else {
80 evlist->last->next = next;
81 evlist->last = next;
82 }
83}
84
85static int
86compare_tm(struct tm *t1, struct tm *t2)
87{
88 if (t1->tm_year != t2->tm_year)
89 return t1->tm_year - t2->tm_year;
90 if (t1->tm_mon != t2->tm_mon)
91 return t1->tm_mon - t2->tm_mon;
92 if (t1->tm_mday != t2->tm_mday)
93 return t1->tm_mday - t2->tm_mday;
94 if (t1->tm_hour != t2->tm_hour)
95 return t1->tm_hour - t2->tm_hour;
96 return t1->tm_min - t2->tm_min;
97}
98
99static int
100compare_event(Event *ev1, Event *ev2)
101{
102 return compare_tm(&ev1->time, &ev2->time);
103}
104
105static Options
106default_op()
107{
108 Options op;
109 time_t t_now = time(NULL);
110 struct tm *now = localtime(&t_now);
111
112 op.format_in = malloc(sizeof(char) * MAXLEN);
113 op.format_out = malloc(sizeof(char) * MAXLEN);
114 op.separator = malloc(sizeof(char) * MAXLEN);
115 strcpy(op.format_in, default_format);
116 strcpy(op.format_out, default_format);
117 strcpy(op.separator, "\t");
118 op.from = *now;
119 op.to = *now;
120
121 return op;
122}
123
124/*
125* Saves the events in ev[] that happen between op->from and op->to in sel[]
126* sorted by date and returns their number.
127*/
128static int
129events_in_range(EventList *evlist, Options op, Event *sel)
130{
131 EventNode *i;
132 int n = 0;
133
134
135 for (i = evlist->first; i != NULL; i = i->next)
136 if (compare_tm(&i->ev.time, &op.from) >= 0 &&
137 compare_tm(&i->ev.time, &op.to) <= 0)
138 sel[n++] = i->ev;
139
140 qsort(sel, n, sizeof(Event),
141 (int (*)(const void *, const void *))compare_event);
142
143 return n;
144}
145
146static char *
147format_line(Event ev, Options op, char *out)
148{
149 strftime(out, MAXLEN, op.format_out, &ev.time);
150 strncat(out, op.separator, MAXLEN - strlen(out));
151 strncat(out, ev.text, MAXLEN - strlen(out));
152
153 return out;
154}
155
156static void
157read_input(Options op, EventList *evlist)
158{
159 struct tm t;
160 char line[MAXLEN], *text_ptr;
161
162 while (fgets(line, MAXLEN, stdin) != NULL)
163 if ((text_ptr = strptime(line, op.format_in, &t)) != NULL)
164 add_event(t, text_ptr, evlist);
165}
166
167static Options
168read_op(int argc, char *argv[])
169{
170 Options op = default_op();
171 int i;
172
173 /* Check for format specification.
174 * This changes the way other options are read */
175 for (i = 1; i < argc; i++) {
176 if (argv[i][0] == '+') {
177 strncpy(op.format_in, &argv[i][1], MAXLEN);
178 strncpy(op.format_out, &argv[i][1], MAXLEN);
179 }
180 }
181
182 for (i = 1; i < argc; i++) {
183 if (!strcmp(argv[i], "-v")) {
184 puts("sdep-"VERSION);
185 exit(0);
186 } else if (!strcmp(argv[i], "-d")) {
187 puts(default_format);
188 exit(0);
189 } else if (!strcmp(argv[i], "-s")) {
190 strncpy(op.separator, argv[++i], MAXLEN);
191 } else if (!strcmp(argv[i], "-f")) {
192 if (i+1 >= argc ||
193 strptime(argv[i+1], op.format_in, &op.from) == NULL)
194 op.from.tm_year = -1000000000; /* Very large number */
195 else
196 i++;
197 } else if (!strcmp(argv[i], "-t")) {
198 if (i+1 >= argc ||
199 strptime(argv[i+1], op.format_in, &op.to) == NULL)
200 op.to.tm_year = 1000000000; /* Very small number */
201 else
202 i++;
203 } else if (!strcmp(argv[i], "-w")) {
204 op.format_out = argv[++i];
205 } else if (argv[i][0] != '+' && strlen(argv[i]) != 0) {
206 fprintf(stderr, "usage: sdep [-dv]");
207 fprintf(stderr, " [+format] [-f [date]] [-t [date]]");
208 fprintf(stderr, " [-w format] [-s string]\n");
209 exit(1);
210 }
211 }
212
213 return op;
214}
215
216static char *
217strtrim(char *t)
218{
219 char *s;
220
221 for (s = &t[strlen(t)-1]; s != t && isspace(*s); *s = '\0', s--);
222 for (; *t != '\0' && isspace(*t); t++);
223
224 return t;
225}
226
227static void
228write_output(Options op, Event *ev, int n)
229{
230 char outline[MAXLEN];
231 int i;
232
233 for (i = 0; i < n; i++)
234 printf("%s\n", format_line(ev[i], op, outline));
235}
236
237int
238main(int argc, char *argv[])
239{
240 Options op;
241 EventList evlist = {0};
242 Event *selected;
243
244 op = read_op(argc, argv);
245 read_input(op, &evlist);
246 selected = malloc(sizeof(Event) * evlist.count);
247 write_output(op, selected, events_in_range(&evlist, op, selected));
248
249 return 0;
250}

Generated with cgit - Back to sebastiano.tronto.net