commit 11a703c761c6ffea6d7237688245f4be492d52bb
parent f7ca86d4c2b5bbbad94d6ab8db7c46c31f4603ca
Author: Andreas Gruhler <agruhl@gmx.ch>
Date: Sun, 4 Jul 2021 18:55:54 +0200
Merge pull request #74 from in0rdr/feature/caldav
Feature/caldav
Diffstat:
M | .gitignore | | | 1 | + |
M | Makefile | | | 16 | ++++++++++------ |
M | README.md | | | 91 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------- |
A | TESTING.md | | | 33 | +++++++++++++++++++++++++++++++++ |
A | config/diary.cfg | | | 17 | +++++++++++++++++ |
D | diary-cheat-sheet.png | | | 0 | |
D | diary.1 | | | 192 | ------------------------------------------------------------------------------- |
D | diary.c | | | 719 | ------------------------------------------------------------------------------- |
D | diary.cfg | | | 11 | ----------- |
D | diary.h | | | 65 | ----------------------------------------------------------------- |
R | demo.gif -> img/demo.gif | | | 0 | |
A | img/diary-cheat-sheet.png | | | 0 | |
A | img/diary-worm.png | | | 0 | |
A | img/diary-worm.svg | | | 1209 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | man/diary.1 | | | 203 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | man/diary.1.html | | | 319 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/caldav.c | | | 877 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/caldav.h | | | 54 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/diary.c | | | 687 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/diary.h | | | 39 | +++++++++++++++++++++++++++++++++++++++ |
A | src/utils.c | | | 335 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/utils.h | | | 58 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
22 files changed, 3914 insertions(+), 1012 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,3 +1,4 @@
diary
*.o
*.gch
+*.swp
diff --git a/Makefile b/Makefile
@@ -1,25 +1,29 @@
TARGET = diary
-SRC = diary.c
+SRCDIR = src/
+_SRC = utils.c caldav.c diary.c
+SRC = $(addprefix $(SRCDIR), $(_SRC))
PREFIX ?= /usr/local
BINDIR ?= $(DESTDIR)$(PREFIX)/bin
MANDIR := $(DESTDIR)$(PREFIX)/share/man
-MAN1 = diary.1
+MAN1 = man/diary.1
CC = gcc
-CFLAGS = -Wall
+CFLAGS = -Wall \
+ -DGOOGLE_OAUTH_CLIENT_ID=\"$(GOOGLE_OAUTH_CLIENT_ID)\" \
+ -DGOOGLE_OAUTH_CLIENT_SECRET=\"$(GOOGLE_OAUTH_CLIENT_SECRET)\"
UNAME = ${shell uname}
ifeq ($(UNAME),FreeBSD)
- LIBS = -lncurses
+ LIBS = -lncurses -lcurl -lpthread
endif
ifeq ($(UNAME),Linux)
- LIBS = -lncursesw
+ LIBS = -lncursesw -lcurl -lpthread
endif
ifeq ($(UNAME),Darwin)
- LIBS = -lncurses -framework CoreFoundation
+ LIBS = -lncurses -lcurl -lpthread -framework CoreFoundation
endif
diff --git a/README.md b/README.md
@@ -1,8 +1,13 @@
-# CLI Diary
+<p align="center">
+<img src="./img/diary-worm.png" width="250px" />
+</p>
-This is a text based diary, inspired by [khal](https://github.com/pimutils/khal). Diary entries are stored in raw text. You may say C & ncurses are old, I say paper is older..
+# Diary
-![Diary Demo](https://raw.githubusercontent.com/in0rdr/diary/master/demo.gif)
+This is a text-based diary, inspired by [khal](https://github.com/pimutils/khal). Journal entries are stored in text files.
+
+## Demo
+![Diary Demo](https://raw.githubusercontent.com/in0rdr/diary/master/img/demo.gif)
## Usage
1. Set the EDITOR environment variable to your favourite text editor:
@@ -24,29 +29,29 @@ This is a text based diary, inspired by [khal](https://github.com/pimutils/khal)
3. Use the keypad or VIM-like shortcuts to move between dates:
```
- e, Enter Edit the current entry
- d, x Delete/remove current entry
- t Jump to today
- s Jump to specific day
+ e, Enter edit current entry
+ d, x delete current entry
+ s sync current entry with CalDAV server
+
+ t jump to today
+ f jump to or find specific day
j, down go forward by 1 week
k, up go backward by 1 week
h, left go left by 1 day
l, right go right by 1 day
+ J go forward by 1 month
+ K go backward by 1 month
- N go to the previous diary entry
- n go to the next diary entry
-
+ N go to the previous journal entry
+ n go to the next journal entry
g go to start of journal
G go to end of journal
- J Go forward by 1 month
- K Go backward by 1 month
-
q quit the program
```
-![diary cheet sheet](https://raw.githubusercontent.com/in0rdr/diary/master/diary-cheat-sheet.png)
+![diary cheet sheet](https://raw.githubusercontent.com/in0rdr/diary/master/img/diary-cheat-sheet.png)
## Install
@@ -65,14 +70,21 @@ Server = https://downloadcontent.opensuse.org/repositories/home:/in0rdr/Arch/$ar
## Build
[![Build Status](https://travis-ci.org/in0rdr/diary.svg?branch=master)](https://travis-ci.org/in0rdr/diary)
+1. Define [OAuth2 application credentials](https://developers.google.com/identity/protocols/oauth2) if CalDAV sync should be effective:
+ ```
+ export GOOGLE_OAUTH_CLIENT_ID=""
+ export GOOGLE_OAUTH_CLIENT_SECRET=""
+ ```
+
+ Alternatively, leave these two variables unset and [define the clientid/secret in the configuration file](#Google-Calendar-OAuth2) at run-time.
-1. Compile (requires ncurses):
+2. Compile (requires ncurses and libcurl):
```
make
```
Note: for *BSD users run gmake.
-2. Install the binary (optional):
+3. Install the binary (optional):
```
sudo make install
```
@@ -83,10 +95,10 @@ Note: for *BSD users run gmake.
## Configuration File
-The [`diary.cfg`](./diary.cfg) configuration file can optionally be used to persist diary configuration. To install the sample from this repository:
+The [`diary.cfg`](./config/diary.cfg) configuration file can optionally be used to persist diary configuration. To install the sample from this repository:
```bash
mkdir -p ${XDG_CONFIG_HOME:-~/.config}/diary
-cp diary.cfg ${XDG_CONFIG_HOME:-~/.config}/diary/
+cp config/diary.cfg ${XDG_CONFIG_HOME:-~/.config}/diary/
```
The file `${XDG_CONFIG_HOME:-~/.config}/diary/diary.cfg` should adhere to a basic `key = value` format. Lines can be commented with the special characters `#` or `;`. The following configuration keys are currently supported:
@@ -94,10 +106,14 @@ The file `${XDG_CONFIG_HOME:-~/.config}/diary/diary.cfg` should adhere to a basi
| Command Line Option | Config Key | Example Config Value | Default Config Value | Description |
| --- | --- | --- | --- | --- |
| `--dir`, `-d`, or first non-option argument | `dir` | ~/diary | n/a | Diary directory. Path that holds the journal text files. If unset, defaults to environment variable `$DIARY_DIR`.|
-| `--editor` or `-e` | `editor` | "vim" | "" | Editor to open journal files with. If unset, defaults to environment variable `$EDITOR`. If no editor is provided, the diary is opened read-only. |
+| `--editor` or `-e` | `editor` | vim | (empty) | Editor to open journal files with. If unset, defaults to environment variable `$EDITOR`. If no editor is provided, the diary is opened read-only. |
| `--fmt` or `-f` | `fmt` | %d_%b_%y | %Y-%m-%d | Date format and file name for the files inside the `dir`. For the format specifiers, see [`man strftime`](https://man7.org/linux/man-pages/man3/strftime.3.html). Be careful: If you change this, you might no longer find your existing diary entries, because the diary assumes to find the journal files under another file name. Hence, a change in FMT shows an empty diary, at first. Rename all files in the DIARY_DIR to migrate to a new FMT. |
| `--range` or `-r` | `range` | 10 | 1 | Number of years to show before/after todays date |
| `--weekday` or `-w` | `weekday` | 0 | 1 | First weekday, `7` = Sunday, `1` = Monday, ..., `6` = Saturday. Use `7` (or `0`) to display week beginning at Sunday ("S-M-T-W-T-F-S"), or `1` for "M-T-W-T-F-S-S". If `glibc` is installed, the first day of the week is derived from the current locale setting (`$LANG`, see `man locale`). Without `glibc`, the first weekday defaults to 1 (Monday), unless specified otherwise with this option. |
+| n/a | `google_calendar` | diary | (empty) | Displayname of Google Calendar for [CalDAV sync](#CalDAV-sync) |
+| n/a | `google_clientid` | 123-123.apps.googleusercontent.com | [built-in](#Build) / (empty) | Google Calendar for [Google Calendar OAuth2](#Google-Calendar-OAuth2) clientid |
+| n/a | `google_secretid` | 321 | [built-in](#Build) / (empty) | Google Calendar for [Google Calendar OAuth2](#Google-Calendar-OAuth2) secretid |
+| n/a | `google_tokenfile` | ~/.diary-token | ~/.diary-token | Displayname of Google Calendar for [Google Calendar OAuth2](#Google-Calendar-OAuth2) API token file|
## Precedence Rules
<a name="precedence_rules"></a>
@@ -137,3 +153,40 @@ $ rm ${XDG_CONFIG_HOME:-~/.config}/diary/diary.cfg
# start with 'weekday' default value from source code (1=Mon)
$ diary
```
+
+## CalDAV Sync
+The journal files can be synced via CalDAV. Currently, only the Google Calendar is supported as remote provider. Please open an [issue](https://github.com/in0rdr/diary/issues) to implement support for additional remote calendar servers.
+
+
+The calender for synchronization can be defined with the [configuration](#Configuration-File) key `google_calendar`:
+```
+# Google calendar name for CalDAV sync
+google_calendar = example
+```
+
+This key is empty by default and the only configuration key required for setting up synchronization.
+
+### Google Calendar OAuth2
+
+The Google Calendar CalDAV API is protected with OAuth2.
+
+The [packaged binaries](#Install) ship with predefined app credentials (clientid/secretid) and consent screen.
+
+The credentials and the consent screen can be redefined at [compile time](#Build) or with the following keys in the [configuration file](#Configuration-File):
+
+```
+# Google OAuth2 clientid and secretid
+google_clientid = 123-123.apps.googleusercontent.com
+google_secretid = 321
+```
+
+The token used to authenticate with the Google API is stored in the file specified by `google_tokenfile` (defaults to `~/.diary-token`):
+```
+# Google OAuth2 tokenfile
+google_tokenfile = ~/.diary-token
+```
+
+The application requires two [OAuth2 scopes](https://developers.google.com/calendar/auth) for CalDAV requests:
+
+1. `https://www.googleapis.com/auth/calendar`: read/write access to Calendars - required to discover the unique hyperlink/URI for the calendar specified by the [configuration key](#Configuration-File) `google_calendar`
+2. `https://www.googleapis.com/auth/calendar.events.owned`: read/write access to Events owned by the user - allows diary to create/read/update/delete events in `google_calendar`
diff --git a/TESTING.md b/TESTING.md
@@ -0,0 +1,33 @@
+# Testing
+
+This file holds notes for testing purposes.
+
+## Render Documentation / Man Page
+
+### Plain Text
+Generate plain text from "runoff" text:
+```bash
+tbl man/diary.1 | nroff -man | less
+```
+
+### `mandoc` HTML (preferred)
+Install [`mandoc`](https://en.wikipedia.org/wiki/Mandoc):
+```bash
+sudo apt-get install mandoc
+```
+
+Generate HTML file:
+```bash
+mandoc -Thtml man/diary.1 > man/diary.1.html
+```
+
+### `groff` HTML
+Install [`groff`](https://www.gnu.org/software/groff):
+```bash
+sudo apt-get install groff
+```
+
+Generate HTML file (`-t` for `tbl`):
+```bash
+groff -t -mandoc -Thtml man/diary.1 > man/diary.1.html
+```
diff --git a/config/diary.cfg b/config/diary.cfg
@@ -0,0 +1,17 @@
+# Path that holds the journal text files
+#dir = ~/diary
+# Number of years to show before/after todays date
+range = 1
+# 0 = Sunday, 1 = Monday, ..., 6 = Saturday
+weekday = 1
+# Date and file format, change with care
+fmt = %Y-%m-%d
+# Editor to open journal files with
+editor =
+# Google calendar name for CalDAV sync
+#google_calendar =
+# Google OAuth2 clientid and secretid
+#google_clientid =
+#google_secretid =
+# Google OAuth2 tokenfile
+#google_tokenfile = ~/.diary-token
diff --git a/diary-cheat-sheet.png b/diary-cheat-sheet.png
Binary files differ.
diff --git a/diary.1 b/diary.1
@@ -1,192 +0,0 @@
-.TH DIARY 1
-.SH NAME
-diary \- Simple text-based diary program
-
-.SH SYNOPSIS
-.B diary
-[\fIOPTION\fR]... [\fIDIRECTORY\fR]...
-.br
-
-.SH DESCRIPTION
-.B diary
-is a simple text-based program for managing journal entries.
-
-.SH OPTIONS
-.TP
-\fB\-v\fR, \fB\-\-version\fR
-Print diary version
-.TP
-\fB\-h\fR, \fB\-\-help\fR
-Show diary help text
-.TP
-\fB\-d\fR, \fB\-\-dir\fR=\fI\,DIARY_DIR\/\fR
-Diary storage directory DIARY_DIR
-.TP
-\fB\-e\fR, \fB\-\-editor\fR=\fI\,EDITOR\/\fR
-EDITOR is the text editor used for opening the journal files.
-.TP
-\fB\-f\fR, \fB\-\-fmt\fR=\fI\,FMT\/\fR
-FMT is a custom date and file format. Change with care, because the diary
-reads and writes to files with file name FMT. The new FMT is only
-applied to newly saved entries. Existing entries with the old FMT are not
-automatically migrated to the new FMT and do not show up with a new FMT
-specifier. Consequently, a change in FMT shows an empty diary at first.
-Rename all files in the DIARY_DIR to migrate to a new FMT.
-.TP
-\fB\-r\fR, \fB\-\-range\fR=\fI\,RANGE\/\fR
-RANGE is the number of years to show before/after todays date. Defaults to 1 year.
-.TP
-\fB\-f\fR, \fB\-\-weekday\fR=\fI\,DAY\/\fR
-First day of the week. DAY is an integer in range (0..6), interpreted as 0 or 7 = Sun,
-1 = Mon, ..., 6 = Sat. If glibc is installed, the first day of the week is derived
-from the current locale setting ('$LANG', see man locale). Without glibc, the
-first weekday defaults to 1 (Monday), unless specified otherwise with this option.
-
-.SH NAVIGATION
-Navigation is done using the following vim-inspired keyboard shortcuts:
-
-.TS
-tab(|);
-l l.
-Key(s) | Action
-====== | ======
-k, up | go backward by 1 week
-j, down | go forward by 1 week
-h, left | go backward by 1 day
-l, right | go forward by 1 day
-J | go forward by 1 month
-K | go backward by 1 month
-
-e, enter | edit current entry
-d, x | delete current entry
-q | quit the program
-
-N | go to the previous journal entry
-n | go to the next journal entry
-g | go to start of journal
-G | go to end of journal
-
-t | jump to today
-s | jump to specific day
-.TE
-
-.SH ENVIRONMENT
-
-.IP DIARY_DIR
-If this variable is set to a directory that can be opened,
-.B diary
-will use it to store diary files. Diary files are simple text files named
-after their date, formatted according to FMT (see '-f'/'--fmt' options and
-'fmt' config key). The format defaults to "%Y-%m-%d", which is "YYYY-MM-DD"
-(see man strftime). All other files different from FMT are ignored.
-
-.IP EDITOR
-The program used to edit journal entries.
-
-.IP LANG
-The default locale used to display the first day of the week.
-
-.SH ARGUMENTS
-
-If the argument \fIDIRECTORY\fR is given, diary files are read from and
-stored to that directory, ignoring the DIARY_DIR environment variable,
-any '-d'/'--dir' options or the 'dir' value set in the config file.
-
-.SH FILES
-.TP
-.I ${PREFIX:-/usr/local/bin}/diary
-The diary binary
-.TP
-.I ${XDG_CONFIG_HOME:-~/.config}/diary/diary.cfg
-An optional diary configuration file
-
-.SH CONFIGURATION FILE
-The diary.cfg configuration file can optionally be used to persist diary configuration.
-
-Create default config location:
-
-.nf
- $ mkdir -p ${XDG_CONFIG_HOME:-~/.config}/diary
-.fi
-
-Install an example config file with defaults:
-
-.nf
- $ echo '# Path that holds the journal text files
- #dir = ~/diary
- # Number of years to show before/after todays date
- range = 1
- # 0 = Sunday, 1 = Monday, ..., 6 = Saturday
- weekday = 1
- # Date and file format, change with care
- fmt = %Y-%m-%d
- # Editor to open journal files with
- editor = ""' | sed 's/^[[:space:]]*//' > ${XDG_CONFIG_HOME:-~/.config}/diary/diary.cfg
-.fi
-
-Use the '#' or ';' characters to comment lines.
-
-.SH PRECEDENCE RULES
-
-The default variables, for instance, for the configuration variables 'editor', 'dir' and 'weekday', are populated with values in the following order:
-
-.TP
-1.
-No default for 'DIARY_DIR'. Defaults for 'range', 'weekday', 'fmt' and 'editor' are provided in 'diary.h'. If 'EDITOR' is unset and no editor is provided in the config file or via the '-e' option, the
-.B
-diary
-works read-only. Journal files cannot be opened. If 'DIARY_DIR' is not provided, the
-.B
-diary
-won't open.
-.TP
-2.
-.B
-Config file
-(empty default for 'editor', no default for 'dir')
-.TP
-3.
-.B
-Environment
-variables '$DIARY_DIR', '$EDITOR' and '$LANG' for locale ('weekday')
-.TP
-4.
-.B
-Option
-arguments, see section
-.B
-OPTIONS
-.TP
-5.
-First non-option argument \fIDIRECTORY\fR is interpreted as 'DIARY_DIR'
-
-.SH PRECEDENCE EXAMPLE: LOCALE AND FIRST DAY OF WEEK
-If glibc is installed, the first weekday defaults to the locale defined in the current shell
-environment ($LANG, see man locale), unless specified otherwise via the '--weekday'/'-w'.
-
-.nf
- # start with weekday=3(Wed), overrule any other configuration value
- $ diary -w3
-
- # start with glibc derived weekday=1, regardless of 'weekday' in config file
- $ LANG=de_CH diary
-
- # if glibc is installed, start with glibc derived base date (weekday=0)
- $ LANG= diary
-
- # disable environment variable, default to value from config file
- $ unset LANG
-
- # start with 'weekday' default from config file, if available
- $ diary
-
- # remove config file
- $ rm ${XDG_CONFIG_HOME:-~/.config}/diary/diary.cfg
-
- # start with 'weekday' default value from source code (1=Mon)
- $ diary
-.fi
-
-.SH DEVELOPMENT
-All source code is available in this github repository:
-<https://github.com/in0rdr/diary/>. Contributions are always welcome!
diff --git a/diary.c b/diary.c
@@ -1,719 +0,0 @@
-#include "diary.h"
-
-int cy, cx;
-time_t raw_time;
-struct tm today;
-struct tm curs_date;
-struct tm cal_start;
-struct tm cal_end;
-
-// normally leap is every 4 years,
-// but is skipped every 100 years,
-// unless it is divisible by 400
-#define is_leap(yr) ((yr % 400 == 0) || (yr % 4 == 0 && yr % 100 != 0))
-
-void setup_cal_timeframe()
-{
- raw_time = time(NULL);
- localtime_r(&raw_time, &today);
- curs_date = today;
-
- cal_start = today;
- cal_start.tm_year -= CONFIG.range;
- cal_start.tm_mon = 0;
- cal_start.tm_mday = 1;
- mktime(&cal_start);
-
- if (cal_start.tm_wday != CONFIG.weekday) {
- // adjust start date to weekday before 01.01
- cal_start.tm_year--;
- cal_start.tm_mon = 11;
- cal_start.tm_mday = 31 - (cal_start.tm_wday - CONFIG.weekday) + 1;
- mktime(&cal_start);
- }
-
- cal_end = today;
- cal_end.tm_year += CONFIG.range;
- cal_end.tm_mon = 11;
- cal_end.tm_mday = 31;
- mktime(&cal_end);
-}
-
-void draw_wdays(WINDOW* head)
-{
- int i;
- for (i = CONFIG.weekday; i < CONFIG.weekday + 7; i++) {
- waddstr(head, WEEKDAYS[i % 7]);
- waddch(head, ' ');
- }
- wrefresh(head);
-}
-
-void draw_calendar(WINDOW* number_pad, WINDOW* month_pad, const char* diary_dir, size_t diary_dir_size)
-{
- struct tm i = cal_start;
- char month[10];
- bool has_entry;
-
- while (mktime(&i) <= mktime(&cal_end)) {
- has_entry = date_has_entry(diary_dir, diary_dir_size, &i);
-
- if (has_entry)
- wattron(number_pad, A_BOLD);
-
- wprintw(number_pad, "%2i", i.tm_mday);
-
- if (has_entry)
- wattroff(number_pad, A_BOLD);
-
- waddch(number_pad, ' ');
-
- // print month in sidebar
- if (i.tm_mday == 1) {
- strftime(month, sizeof month, "%b", &i);
- getyx(number_pad, cy, cx);
- mvwprintw(month_pad, cy, 0, "%s ", month);
- }
-
- i.tm_mday++;
- mktime(&i);
- }
-}
-
-/* Update the header with the cursor date */
-void update_date(WINDOW* header)
-{
- char dstr[16];
- mktime(&curs_date);
- get_date_str(&curs_date, dstr, sizeof dstr);
-
- wclear(header);
- mvwaddstr(header, 0, 0, dstr);
- wrefresh(header);
-}
-
-bool go_to(WINDOW* calendar, WINDOW* aside, time_t date, int* cur_pad_pos)
-{
- if (date < mktime(&cal_start) || date > mktime(&cal_end))
- return false;
-
- int diff_seconds = date - mktime(&cal_start);
- int diff_days = diff_seconds / 60 / 60 / 24;
- int diff_weeks = diff_days / 7;
- int diff_wdays = diff_days % 7;
-
- localtime_r(&date, &curs_date);
-
- getyx(calendar, cy, cx);
-
- // remove the STANDOUT attribute from the day we are leaving
- chtype current_attrs = mvwinch(calendar, cy, cx) & A_ATTRIBUTES;
- // leave every attr as is, but turn off STANDOUT
- current_attrs &= ~A_STANDOUT;
- mvwchgat(calendar, cy, cx, 2, current_attrs, 0, NULL);
-
- // add the STANDOUT attribute to the day we are entering
- chtype new_attrs = mvwinch(calendar, diff_weeks, diff_wdays * 3) & A_ATTRIBUTES;
- new_attrs |= A_STANDOUT;
- mvwchgat(calendar, diff_weeks, diff_wdays * 3, 2, new_attrs, 0, NULL);
-
- if (diff_weeks < *cur_pad_pos)
- *cur_pad_pos = diff_weeks;
- if (diff_weeks > *cur_pad_pos + LINES - 2)
- *cur_pad_pos = diff_weeks - LINES + 2;
- prefresh(aside, *cur_pad_pos, 0, 1, 0, LINES - 1, ASIDE_WIDTH);
- prefresh(calendar, *cur_pad_pos, 0, 1, ASIDE_WIDTH, LINES - 1, ASIDE_WIDTH + CAL_WIDTH);
-
- return true;
-}
-
-/* Update window 'win' with diary entry from date 'date' */
-void display_entry(const char* dir, size_t dir_size, const struct tm* date, WINDOW* win, int width)
-{
- char path[100];
- char* ppath = path;
- int c;
-
- // get entry path
- fpath(dir, dir_size, date, &ppath, sizeof path);
- if (ppath == NULL) {
- fprintf(stderr, "Error while retrieving file path for diary reading");
- return;
- }
-
- wclear(win);
-
- if (date_has_entry(dir, dir_size, date)) {
- FILE* fp = fopen(path, "r");
- if (fp == NULL) perror("Error opening file");
-
- wmove(win, 0, 0);
- while((c = getc(fp)) != EOF) waddch(win, c);
-
- fclose(fp);
- }
-
- wrefresh(win);
-}
-
-/* Writes edit command for 'date' entry to 'rcmd'. '*rcmd' is NULL on error. */
-void edit_cmd(const char* dir, size_t dir_size, const struct tm* date, char** rcmd, size_t rcmd_size)
-{
- // set the edit command to env editor
- if (strlen(CONFIG.editor) + 2 > rcmd_size) {
- fprintf(stderr, "Binary path of default editor too long");
- *rcmd = NULL;
- return;
- }
- strcpy(*rcmd, CONFIG.editor);
- strcat(*rcmd, " ");
-
- // get path of entry
- char path[100];
- char* ppath = path;
- fpath(dir, dir_size, date, &ppath, sizeof path);
-
- if (ppath == NULL) {
- fprintf(stderr, "Error while retrieving file path for editing");
- *rcmd = NULL;
- return;
- }
-
- // concatenate editor command with entry path
- if (strlen(*rcmd) + strlen(path) + 1 > rcmd_size) {
- fprintf(stderr, "Edit command too long");
- return;
- }
- strcat(*rcmd, path);
-}
-
-bool date_has_entry(const char* dir, size_t dir_size, const struct tm* i)
-{
- char epath[100];
- char* pepath = epath;
-
- // get entry path and check for existence
- fpath(dir, dir_size, i, &pepath, sizeof epath);
-
- if (pepath == NULL) {
- fprintf(stderr, "Error while retrieving file path for checking entry existence");
- return false;
- }
-
- return (access(epath, F_OK) != -1);
-}
-
-void get_date_str(const struct tm* date, char* date_str, size_t date_str_size)
-{
- strftime(date_str, date_str_size, CONFIG.fmt, date);
-}
-
-/* Writes file path for 'date' entry to 'rpath'. '*rpath' is NULL on error. */
-void fpath(const char* dir, size_t dir_size, const struct tm* date, char** rpath, size_t rpath_size)
-{
- // check size of result path
- if (dir_size + 1 > rpath_size) {
- fprintf(stderr, "Directory path too long");
- *rpath = NULL;
- return;
- }
-
- // add path of the diary dir to result path
- strcpy(*rpath, dir);
-
- // check for terminating '/' in path
- if (dir[dir_size - 1] != '/') {
- // check size again to accommodate '/'
- if (dir_size + 1 > rpath_size) {
- fprintf(stderr, "Directory path too long");
- *rpath = NULL;
- return;
- }
- strcat(*rpath, "/");
- }
-
- char dstr[16];
- get_date_str(date, dstr, sizeof dstr);
-
- // append date to the result path
- if (strlen(*rpath) + strlen(dstr) > rpath_size) {
- fprintf(stderr, "File path too long");
- *rpath = NULL;
- return;
- }
- strcat(*rpath, dstr);
-}
-
-/*
- * Finds the date with a diary entry closest to <current>.
- * Depending on <search_backwards>, it will find the most recent
- * previous date or the oldest next date with an entry. If no
- * entry is found within the calendar size, <current> is returned.
- */
-struct tm find_closest_entry(const struct tm current,
- bool search_backwards,
- const char* diary_dir,
- size_t diary_dir_size)
-{
- time_t end_time = mktime(&cal_end);
- time_t start_time = mktime(&cal_start);
-
- int step = search_backwards ? -1 : +1;
-
- struct tm it = current;
- it.tm_mday += step;
- time_t it_time = mktime(&it);
-
- for( ; it_time >= start_time && it_time <= end_time; it_time = mktime(&it)) {
-
- if (date_has_entry(diary_dir, diary_dir_size, &it)) {
- return it;
- }
- it.tm_mday += step;
- }
-
- return current;
-}
-
-bool read_config(const char* file_path)
-{
- wordexp_t config_file_path_wordexp;
- char config_file_path[256];
-
- if ( wordexp( file_path, &config_file_path_wordexp, 0 ) == 0) {
- if (strlen(config_file_path_wordexp.we_wordv[0]) + 1 > sizeof config_file_path) {
- fprintf(stderr, "Config file path '%s' too long\n", config_file_path_wordexp.we_wordv[0]);
- return false;
- }
- strcpy(config_file_path, config_file_path_wordexp.we_wordv[0]);
- }
- wordfree(&config_file_path_wordexp);
-
- // check if config file is readable
- if( access( config_file_path, R_OK ) != 0 ) {
- fprintf(stderr, "Config file '%s' not readable, skipping\n", config_file_path);
- return false;
- }
-
- char key_buf[80];
- char value_buf[80];
- char line[256];
- FILE * pfile;
-
- // read config file line by line
- pfile = fopen(config_file_path, "r");
- while (fgets(line, sizeof line, pfile)) {
- // ignore comment lines
- if (*line == '#' || *line == ';') continue;
-
- if (sscanf(line, "%s = %s", key_buf, value_buf) == 2) {
- if (strcmp("dir", key_buf) == 0) {
- wordexp_t diary_dir_wordexp;
- if ( wordexp( value_buf, &diary_dir_wordexp, 0 ) == 0) {
- // set expanded diary directory path from config file
- CONFIG.dir = (char *) calloc(strlen(diary_dir_wordexp.we_wordv[0]) + 1, sizeof(char));
- strcpy(CONFIG.dir, diary_dir_wordexp.we_wordv[0]);
- }
- wordfree(&diary_dir_wordexp);
- } else if (strcmp("range", key_buf) == 0) {
- CONFIG.range = atoi(value_buf);
- } else if (strcmp("weekday", key_buf) == 0) {
- CONFIG.weekday = atoi(value_buf);
- } else if (strcmp("fmt", key_buf) == 0) {
- CONFIG.fmt = (char *) malloc(strlen(value_buf) + 1 * sizeof(char));
- strcpy(CONFIG.fmt, value_buf);
- } else if (strcmp("editor", key_buf) == 0) {
- CONFIG.editor = (char *) malloc(strlen(value_buf) + 1 * sizeof(char));
- strcpy(CONFIG.editor, value_buf);
- }
- }
- }
- fclose (pfile);
- return true;
-}
-
-void usage() {
- printf("Usage : diary [OPTION]... [DIRECTORY]...\n");
- printf("\n");
- printf("Simple CLI diary (v%s)\n", DIARY_VERSION);
- printf("Edit journal entries from the command line\n");
- printf("\n");
- printf("Options:\n");
- printf(" -v, --version : Print diary version\n");
- printf(" -h, --help : Show diary help text\n");
- printf(" -d, --dir DIARY_DIR : Diary storage directory DIARY_DIR\n");
- printf(" -e, --editor EDITOR : Editor to open journal files with\n");
- printf(" -f, --fmt FMT : Date and file format, change with care\n");
- printf(" -r, --range RANGE : RANGE is the number of years to show before/after today's date\n");
- printf(" -w, --weekday DAY : First day of the week, 0 = Sun, 1 = Mon, ..., 6 = Sat\n");
- printf("\n");
- printf("Full docs and keyboard shortcuts: 'man diary'\n");
- printf("or online via: <https://github.com/in0rdr/diary>\n");
-}
-
-int main(int argc, char** argv) {
- setlocale(LC_ALL, "");
- char* env_var;
- char* config_home;
- char* config_file_path;
- chtype atrs;
-
- // the diary directory defaults to the diary_dir specified in the config file
- config_home = getenv("XDG_CONFIG_HOME");
- if (config_home == NULL) config_home = XDG_CONFIG_HOME_FALLBACK;
- // concat config home with the file path to the config file
- config_file_path = (char *) calloc(strlen(config_home) + strlen(CONFIG_FILE_PATH) + 1, sizeof(char));
- sprintf(config_file_path, "%s/%s", config_home, CONFIG_FILE_PATH);
- // read config from config file path
- read_config(config_file_path);
-
- // get diary directory from environment
- env_var = getenv("DIARY_DIR");
- if (env_var != NULL) {
- // if available, overwrite the diary directory with the environment variable
- CONFIG.dir = (char *) calloc(strlen(env_var) + 1, sizeof(char));
- strcpy(CONFIG.dir, env_var);
- }
-
- // get editor from environment
- env_var = getenv("EDITOR");
- if (env_var != NULL) {
- // if available, overwrite the editor with the environments EDITOR
- CONFIG.editor = (char *) calloc(strlen(env_var) + 1, sizeof(char));
- strcpy(CONFIG.editor, env_var);
- }
-
- // get locale from environment variable LANG
- // https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html
- env_var = getenv("LANG");
- if (env_var != NULL) {
- // if available, overwrite the editor with the environments locale
- #ifdef __GNU_LIBRARY__
- // references: locale(5) and util-linux's cal.c
- // get the base date, 8-digit integer (YYYYMMDD) returned as char *
- #ifdef _NL_TIME_WEEK_1STDAY
- unsigned long d = (uintptr_t) nl_langinfo(_NL_TIME_WEEK_1STDAY);
- // reference: https://sourceware.org/glibc/wiki/Locales
- // assign a static date value 19971130 (a Sunday)
- #else
- unsigned long d = 19971130;
- #endif
- struct tm base = {
- .tm_sec = 0,
- .tm_min = 0,
- .tm_hour = 0,
- .tm_mday = d % 100,
- .tm_mon = (d / 100) % 100 - 1,
- .tm_year = d / (100 * 100) - 1900
- };
- mktime(&base);
- // weekday is base date's day of the week offset by (_NL_TIME_FIRST_WEEKDAY - 1)
- #ifdef __linux__
- CONFIG.weekday = (base.tm_wday + *nl_langinfo(_NL_TIME_FIRST_WEEKDAY) - 1) % 7;
- #elif defined __MACH__
- CFIndex first_day_of_week;
- CFCalendarRef currentCalendar = CFCalendarCopyCurrent();
- first_day_of_week = CFCalendarGetFirstWeekday(currentCalendar);
- CFRelease(currentCalendar);
- CONFIG.weekday = (base.tm_wday + first_day_of_week - 1) % 7;
- #endif
- #endif
- }
-
- // get the diary directory via argument, this takes precedence over env/config
- if (argc < 2) {
- if (CONFIG.dir == NULL) {
- fprintf(stderr, "The diary directory must be provided as (non-option) arg, `--dir` arg,\n"
- "or in the DIARY_DIR environment variable, see `diary --help` or DIARY(1)\n");
- return 1;
- }
- } else {
- int option_char;
- int option_index = 0;
-
- // define options, see GETOPT(3)
- static const struct option long_options[] = {
- { "version", no_argument, 0, 'v' },
- { "help", no_argument, 0, 'h' },
- { "dir", required_argument, 0, 'd' },
- { "editor", required_argument, 0, 'e' },
- { "fmt", required_argument, 0, 'f' },
- { "range", required_argument, 0, 'r' },
- { "weekday", required_argument, 0, 'w' },
- { 0, 0, 0, 0 }
- };
-
- // read option characters
- while (1) {
- option_char = getopt_long(argc, argv, "vhd:r:w:f:e:", long_options, &option_index);
-
- if (option_char == -1) {
- break;
- }
-
- switch (option_char) {
- case 'v':
- // show program version
- printf("v%s\n", DIARY_VERSION);
- return 0;
- break;
- case 'h':
- // show help text
- // printf("see man(1) diary\n");
- usage();
- return 0;
- break;
- case 'd':
- // set diary directory from option character
- CONFIG.dir = (char *) calloc(strlen(optarg) + 1, sizeof(char));
- strcpy(CONFIG.dir, optarg);
- break;
- case 'r':
- // set year range from option character
- CONFIG.range = atoi(optarg);
- break;
- case 'w':
- // set first week day from option character
- fprintf(stderr, "%i\n", atoi(optarg));
- CONFIG.weekday = atoi(optarg);
- break;
- case 'f':
- // set date format from option character
- CONFIG.fmt = (char *) calloc(strlen(optarg) + 1, sizeof(char));
- strcpy(CONFIG.fmt, optarg);
- break;
- case 'e':
- // set default editor from option character
- CONFIG.editor = (char *) calloc(strlen(optarg) + 1, sizeof(char));
- strcpy(CONFIG.editor, optarg);
- break;
- default:
- printf("?? getopt returned character code 0%o ??\n", option_char);
- }
- }
-
- if (optind < argc) {
- // set diary directory from first non-option argv-element,
- // required for backwarad compatibility with diary <= 0.4
- CONFIG.dir = (char *) calloc(strlen(argv[optind]) + 1, sizeof(char));
- strcpy(CONFIG.dir, argv[optind]);
- }
- }
-
- // check if that directory exists
- DIR* diary_dir_ptr = opendir(CONFIG.dir);
- if (diary_dir_ptr) {
- // directory exists, continue
- closedir(diary_dir_ptr);
- } else if (errno == ENOENT) {
- fprintf(stderr, "The directory '%s' does not exist\n", CONFIG.dir);
- return 2;
- } else {
- fprintf(stderr, "The directory '%s' could not be opened\n", CONFIG.dir);
- return 1;
- }
-
- setup_cal_timeframe();
-
- initscr();
- raw();
- curs_set(0);
-
- WINDOW* header = newwin(1, COLS - CAL_WIDTH - ASIDE_WIDTH, 0, ASIDE_WIDTH + CAL_WIDTH);
- wattron(header, A_BOLD);
- update_date(header);
- WINDOW* wdays = newwin(1, 3 * 7, 0, ASIDE_WIDTH);
- draw_wdays(wdays);
-
- WINDOW* aside = newpad((CONFIG.range * 2 + 1) * 12 * MAX_MONTH_HEIGHT, ASIDE_WIDTH);
- WINDOW* cal = newpad((CONFIG.range * 2 + 1) * 12 * MAX_MONTH_HEIGHT, CAL_WIDTH);
- keypad(cal, TRUE);
- draw_calendar(cal, aside, CONFIG.dir, strlen(CONFIG.dir));
-
- int ch, conf_ch;
- int pad_pos = 0;
- int syear = 0, smonth = 0, sday = 0;
- struct tm new_date;
- int prev_width = COLS - ASIDE_WIDTH - CAL_WIDTH;
- int prev_height = LINES - 1;
- size_t diary_dir_size = strlen(CONFIG.dir);
-
- bool mv_valid = go_to(cal, aside, raw_time, &pad_pos);
- // mark current day
- atrs = winch(cal) & A_ATTRIBUTES;
- wchgat(cal, 2, atrs | A_UNDERLINE, 0, NULL);
- prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH, LINES - 1, ASIDE_WIDTH + CAL_WIDTH);
-
- WINDOW* prev = newwin(prev_height, prev_width, 1, ASIDE_WIDTH + CAL_WIDTH);
- display_entry(CONFIG.dir, diary_dir_size, &today, prev, prev_width);
-
-
- do {
- ch = wgetch(cal);
- // new_date represents the desired date the user wants to go_to(),
- // which may not be a feasible date at all
- new_date = curs_date;
- char ecmd[150];
- char* pecmd = ecmd;
- char pth[100];
- char* ppth = pth;
- char dstr[16];
- edit_cmd(CONFIG.dir, diary_dir_size, &new_date, &pecmd, sizeof ecmd);
-
- switch(ch) {
- // basic movements
- case 'j':
- case KEY_DOWN:
- new_date.tm_mday += 7;
- mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
- break;
- case 'k':
- case KEY_UP:
- new_date.tm_mday -= 7;
- mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
- break;
- case 'l':
- case KEY_RIGHT:
- new_date.tm_mday++;
- mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
- break;
- case 'h':
- case KEY_LEFT:
- new_date.tm_mday--;
- mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
- break;
-
- // jump to top/bottom of page
- case 'g':
- new_date = find_closest_entry(cal_start, false, CONFIG.dir, diary_dir_size);
- mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
- break;
- case 'G':
- new_date = find_closest_entry(cal_end, true, CONFIG.dir, diary_dir_size);
- mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
- break;
-
- // jump backward/forward by a month
- case 'K':
- if (new_date.tm_mday == 1)
- new_date.tm_mon--;
- new_date.tm_mday = 1;
- mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
- break;
- case 'J':
- new_date.tm_mon++;
- new_date.tm_mday = 1;
- mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
- break;
-
- // search for specific date
- case 's':
- wclear(header);
- curs_set(2);
- mvwprintw(header, 0, 0, "Go to date [YYYY-MM-DD]: ");
- if (wscanw(header, "%4i-%2i-%2i", &syear, &smonth, &sday) == 3) {
- // struct tm.tm_year: years since 1900
- new_date.tm_year = syear - 1900;
- // struct tm.tm_mon in range [0, 11]
- new_date.tm_mon = smonth - 1;
- new_date.tm_mday = sday;
- mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
- }
- curs_set(0);
- //update_date(header);
- break;
- // today shortcut
- case 't':
- new_date = today;
- mv_valid = go_to(cal, aside, raw_time, &pad_pos);
- break;
- // delete entry
- case 'd':
- case 'x':
- if (date_has_entry(CONFIG.dir, diary_dir_size, &curs_date)) {
- // get file path of entry and delete entry
- fpath(CONFIG.dir, diary_dir_size, &curs_date, &ppth, sizeof pth);
- if (ppth == NULL) {
- fprintf(stderr, "Error retrieving file path for entry removal");
- break;
- }
-
- // prepare header for confirmation dialogue
- wclear(header);
- curs_set(2);
- noecho();
-
- // ask for confirmation
- get_date_str(&curs_date, dstr, sizeof dstr);
- mvwprintw(header, 0, 0, "Delete entry '%s'? [Y/n] ", dstr);
- bool conf = false;
- while (!conf) {
- conf_ch = wgetch(header);
- if (conf_ch == 'y' || conf_ch == 'Y' || conf_ch == '\n') {
- if (unlink(pth) != -1) {
- // file successfully deleted, remove entry highlight
- atrs = winch(cal) & A_ATTRIBUTES;
- wchgat(cal, 2, atrs & ~A_BOLD, 0, NULL);
- prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH,
- LINES - 1, ASIDE_WIDTH + CAL_WIDTH);
- }
- } else if (conf_ch == 27 || conf_ch == 'n') {
- update_date(header);
- }
- break;
- }
-
- echo();
- curs_set(0);
- }
- break;
- // edit/create a diary entry
- case 'e':
- case '\n':
- if (pecmd == NULL) {
- fprintf(stderr, "Error retrieving edit command");
- break;
- }
- curs_set(1);
- system(ecmd);
- curs_set(0);
- keypad(cal, TRUE);
-
- // mark newly created entry
- if (date_has_entry(CONFIG.dir, diary_dir_size, &curs_date)) {
- atrs = winch(cal) & A_ATTRIBUTES;
- wchgat(cal, 2, atrs | A_BOLD, 0, NULL);
-
- // refresh the calendar to add highlighting
- prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH,
- LINES - 1, ASIDE_WIDTH + CAL_WIDTH);
- }
- break;
- // Move to the previous diary entry
- case 'N':
- new_date = find_closest_entry(new_date, true, CONFIG.dir, diary_dir_size);
- mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
- break;
- // Move to the next diary entry
- case 'n':
- new_date = find_closest_entry(new_date, false, CONFIG.dir, diary_dir_size);
- mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
- break;
- }
-
- if (mv_valid) {
- update_date(header);
-
- // adjust prev width (if terminal was resized in the mean time)
- prev_width = COLS - ASIDE_WIDTH - CAL_WIDTH;
- wresize(prev, prev_height, prev_width);
-
- // read the diary
- display_entry(CONFIG.dir, diary_dir_size, &curs_date, prev, prev_width);
- }
- } while (ch != 'q');
-
- endwin();
- system("clear");
- return 0;
-}
diff --git a/diary.cfg b/diary.cfg
@@ -1,10 +0,0 @@
-# Path that holds the journal text files
-#dir = ~/diary
-# Number of years to show before/after todays date
-range = 1
-# 0 = Sunday, 1 = Monday, ..., 6 = Saturday
-weekday = 1
-# Date and file format, change with care
-fmt = %Y-%m-%d
-# Editor to open journal files with
-editor = ""
-\ No newline at end of file
diff --git a/diary.h b/diary.h
@@ -1,65 +0,0 @@
-#ifndef DIARY
-#define DIARY
-
-
-#ifdef __MACH__
- #include <CoreFoundation/CoreFoundation.h>
-#endif
-#include <stdio.h>
-#include <stdint.h>
-#include <stdlib.h>
-#include <wordexp.h>
-#include <unistd.h>
-#include <getopt.h>
-#include <string.h>
-#include <time.h>
-#include <errno.h>
-#include <dirent.h>
-#include <ncurses.h>
-#include <locale.h>
-#include <langinfo.h>
-
-#define XDG_CONFIG_HOME_FALLBACK "~/.config"
-#define CONFIG_FILE_PATH "diary/diary.cfg"
-#define DIARY_VERSION "0.6-unstable"
-#define CAL_WIDTH 21
-#define ASIDE_WIDTH 4
-#define MAX_MONTH_HEIGHT 6
-
-static const char* WEEKDAYS[] = {"Su","Mo","Tu","We","Th","Fr","Sa"};
-
-void setup_cal_timeframe();
-void draw_wdays(WINDOW* head);
-void draw_calendar(WINDOW* number_pad, WINDOW* month_pad, const char* diary_dir, size_t diary_dir_size);
-void update_date(WINDOW* header);
-
-bool go_to(WINDOW* calendar, WINDOW* aside, time_t date, int* cur_pad_pos);
-void display_entry(const char* dir, size_t dir_size, const struct tm* date, WINDOW* win, int width);
-void edit_cmd(const char* dir, size_t dir_size, const struct tm* date, char** rcmd, size_t rcmd_size);
-
-bool date_has_entry(const char* dir, size_t dir_size, const struct tm* i);
-void get_date_str(const struct tm* date, char* date_str, size_t date_str_size);
-void fpath(const char* dir, size_t dir_size, const struct tm* date, char** rpath, size_t rpath_size);
-
-typedef struct
-{
- // Path that holds the journal text files
- char* dir;
- // Number of years to show before/after todays date
- int range;
- // 7 = Sunday, 1 = Monday, ..., 6 = Saturday
- int weekday;
- // 2020-12-31
- char* fmt;
- // Editor to open journal files with
- char* editor;
-} config;
-
-config CONFIG = {
- .range = 1,
- .weekday = 1,
- .fmt = "%Y-%m-%d",
- .editor = ""
-};
-
-#endif
diff --git a/demo.gif b/img/demo.gif
Binary files differ.
diff --git a/img/diary-cheat-sheet.png b/img/diary-cheat-sheet.png
Binary files differ.
diff --git a/img/diary-worm.png b/img/diary-worm.png
Binary files differ.
diff --git a/img/diary-worm.svg b/img/diary-worm.svg
@@ -0,0 +1,1209 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="210mm"
+ height="297mm"
+ viewBox="0 0 210 297"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
+ sodipodi:docname="diary-worm.svg">
+ <defs
+ id="defs2">
+ <linearGradient
+ id="linearGradient1883"
+ inkscape:collect="always">
+ <stop
+ id="stop1879"
+ offset="0"
+ style="stop-color:#ffb030;stop-opacity:1" />
+ <stop
+ id="stop1881"
+ offset="1"
+ style="stop-color:#ffd52b;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient1456">
+ <stop
+ style="stop-color:#b20000;stop-opacity:1;"
+ offset="0"
+ id="stop1452" />
+ <stop
+ style="stop-color:#890000;stop-opacity:1"
+ offset="1"
+ id="stop1454" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient1442"
+ inkscape:collect="always">
+ <stop
+ id="stop1438"
+ offset="0"
+ style="stop-color:#f25600;stop-opacity:1" />
+ <stop
+ id="stop1440"
+ offset="1"
+ style="stop-color:#b21000;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient1436"
+ inkscape:collect="always">
+ <stop
+ id="stop1432"
+ offset="0"
+ style="stop-color:#d44b00;stop-opacity:1" />
+ <stop
+ id="stop1434"
+ offset="1"
+ style="stop-color:#930000;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient1413"
+ inkscape:collect="always">
+ <stop
+ id="stop1409"
+ offset="0"
+ style="stop-color:#2d1a0c;stop-opacity:1" />
+ <stop
+ id="stop1411"
+ offset="1"
+ style="stop-color:#8f5127;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient1354">
+ <stop
+ style="stop-color:#e85200;stop-opacity:1"
+ offset="0"
+ id="stop1350" />
+ <stop
+ style="stop-color:#8c0000;stop-opacity:1"
+ offset="1"
+ id="stop1352" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient1322">
+ <stop
+ style="stop-color:#ffb030;stop-opacity:1"
+ offset="0"
+ id="stop1318" />
+ <stop
+ style="stop-color:#ffd21c;stop-opacity:1"
+ offset="1"
+ id="stop1320" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient1314">
+ <stop
+ style="stop-color:#d40000;stop-opacity:1;"
+ offset="0"
+ id="stop1310" />
+ <stop
+ style="stop-color:#8c0000;stop-opacity:1"
+ offset="1"
+ id="stop1312" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient1308"
+ inkscape:collect="always">
+ <stop
+ id="stop1304"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1" />
+ <stop
+ id="stop1306"
+ offset="1"
+ style="stop-color:#dcdcdc;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient1286">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1"
+ offset="0"
+ id="stop1282" />
+ <stop
+ style="stop-color:#dcdcdc;stop-opacity:1"
+ offset="1"
+ id="stop1284" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1308"
+ id="linearGradient1288"
+ x1="185.02104"
+ y1="226.82904"
+ x2="261.72729"
+ y2="296.76712"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1286"
+ id="linearGradient1296"
+ x1="106.10031"
+ y1="235.19983"
+ x2="185.09486"
+ y2="307.37869"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1314"
+ id="linearGradient1316"
+ x1="245.34079"
+ y1="359.60199"
+ x2="251.24689"
+ y2="375.35629"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1883"
+ id="linearGradient1324"
+ x1="299.21814"
+ y1="505.74869"
+ x2="420.54648"
+ y2="342.06714"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1322"
+ id="linearGradient1332"
+ x1="256.48434"
+ y1="393.16736"
+ x2="237.22119"
+ y2="309.12088"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1413"
+ id="linearGradient1340"
+ x1="65.485855"
+ y1="52.828674"
+ x2="70.372467"
+ y2="63.85075"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1413"
+ id="linearGradient1348"
+ x1="30.371655"
+ y1="59.90826"
+ x2="31.488749"
+ y2="68.40992"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1354"
+ id="linearGradient1356"
+ x1="41.471386"
+ y1="26.772978"
+ x2="31.760284"
+ y2="56.625553"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1442"
+ id="linearGradient1364"
+ x1="192.24294"
+ y1="98.161057"
+ x2="206.5248"
+ y2="212.80951"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1436"
+ id="linearGradient1372"
+ x1="221.94862"
+ y1="96.749107"
+ x2="285.1926"
+ y2="188.10391"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1456"
+ id="linearGradient1458"
+ x1="115.77007"
+ y1="50.074276"
+ x2="85.280258"
+ y2="44.604923"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.70710678"
+ inkscape:cx="112.36523"
+ inkscape:cy="561.98773"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer9"
+ showgrid="false"
+ inkscape:window-width="1848"
+ inkscape:window-height="1016"
+ inkscape:window-x="72"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ showguides="false" />
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:groupmode="layer"
+ id="layer9"
+ inkscape:label="Console shading">
+ <path
+ style="fill:#b4b4b4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ id="path5087"
+ d="m 66.089003,163.78854 c 1.590855,-0.22156 3.115789,-0.78617 4.639863,-1.26947 3.657166,-1.15972 0.921628,-0.27555 4.948682,-1.59221 4.00106,-1.36046 8.024852,-2.66157 11.98799,-4.13124 2.068467,-0.76707 2.722261,-1.05544 4.705479,-1.87194 1.992339,-0.80745 3.876654,-1.85253 5.856222,-2.68812 0.729797,-0.30805 0.967425,-0.3765 1.681956,-0.62018 0.203025,-0.0603 0.406045,-0.12057 0.609075,-0.18085 0,0 -5.169621,5.44719 -5.169621,5.44719 v 0 c -0.16741,0.0388 -0.33482,0.0776 -0.50223,0.11644 -2.306174,0.66171 -4.464277,1.72645 -6.730698,2.50442 -2.056646,0.72598 -2.239976,0.80236 -4.402529,1.51378 -4.000077,1.3159 -8.033742,2.54164 -11.949157,4.10105 -0.966407,0.36942 -1.934636,0.73411 -2.899222,1.10825 -3.332449,1.2926 -1.574427,0.6347 -4.547828,1.80186 -0.529672,0.20791 -1.064451,0.40273 -1.593099,0.61323 -0.889132,0.35405 -1.191654,0.49192 -1.962271,0.82952 0,0 5.327388,-5.68173 5.327388,-5.68173 z"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#b4b4b4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ id="path5093"
+ d="m 95.037986,163.89434 c 1.297521,0.48144 2.703277,0.63172 4.071125,0.75173 2.762309,0.24235 2.967489,0.18254 5.944759,0.21737 5.06893,-0.0768 10.13523,-0.47026 15.16285,-1.11867 0.91053,-0.11743 1.8166,-0.26726 2.72491,-0.40088 0,0 -4.22597,4.30048 -4.22597,4.30048 v 0 c -1.85003,0.0365 -3.60174,0.0627 -5.45937,0.13949 -3.99217,0.16503 -7.97951,0.45021 -11.95406,0.86029 -2.905298,0.24347 -3.927587,0.35759 -6.716375,0.49327 -0.752549,0.0366 -1.506418,0.0402 -2.259126,0.0734 -1.130419,0.0499 -1.61789,0.1322 -2.626448,0.11315 -0.122748,-0.002 -0.244483,-0.0228 -0.366725,-0.0342 0,0 5.70443,-5.39544 5.70443,-5.39544 z"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#b4b4b4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ id="path5119"
+ d="m 46.035851,187.54649 c 0.944981,-0.17003 1.896634,-0.30063 2.841887,-0.46884 1.025615,-0.1825 2.039895,-0.43236 3.07177,-0.5753 3.338716,-0.46251 7.936323,-0.75227 11.154905,-0.94446 2.804289,-0.16745 5.610865,-0.29414 8.416298,-0.44121 14.361678,-0.52339 28.718329,-1.62181 42.973669,-3.46415 3.04583,-0.39364 6.07915,-0.87873 9.11872,-1.3181 0,0 -7.7042,7.85131 -7.7042,7.85131 v 0 c -2.71408,0.10636 -5.43008,0.17169 -8.14224,0.31908 -6.25987,0.34018 -14.899197,1.02586 -21.054792,1.58695 -3.807571,0.34706 -7.617063,0.68347 -11.414357,1.12914 -3.753348,0.4405 -7.489172,1.01928 -11.233759,1.52893 -6.669296,1.16326 -10.747248,1.7283 -17.134419,3.33949 -1.889985,0.47676 -3.75499,1.05162 -5.608852,1.65373 -2.121974,0.68919 -4.275373,1.50751 -6.224524,2.61875 -0.384304,0.2191 -0.707024,0.53187 -1.060537,0.79781 0,0 12.000431,-13.61313 12.000431,-13.61313 z"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#b4b4b4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ id="path5129"
+ d="m 172.01488,123.17583 c -2.13834,3.60598 -3.63704,7.53266 -5.18952,11.41131 -2.86134,7.14865 -2.27257,5.6949 -5.17104,13.2026 -3.25472,8.61273 -6.4755,17.2474 -9.26731,26.02334 0,0 -12.76449,8.52502 -12.76449,8.52502 v 0 c 1.48084,-2.96099 2.68839,-5.32945 4.10473,-8.38495 2.90971,-6.27711 5.55413,-12.67808 7.71406,-19.25534 0.67078,-2.16481 1.38806,-4.31575 2.01234,-6.49442 1.40317,-4.89699 2.44257,-9.8883 3.41995,-14.88376 0,0 15.14128,-10.1438 15.14128,-10.1438 z"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#b4b4b4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ id="path5135"
+ d="m 49.878728,144.93859 c -0.168793,0.70998 -0.331224,1.42151 -0.506383,2.12995 -0.843727,3.41253 -1.274988,4.88733 -2.09423,8.50912 -0.404199,1.78693 -0.762064,3.58402 -1.143455,5.37595 -1.475793,6.93388 -0.91703,4.31616 -2.392117,11.1894 -1.141687,5.30949 -2.345542,10.60506 -3.454638,15.92151 0,0 -13.071721,8.93184 -13.071721,8.93184 v 0 c 2.096437,-5.59295 4.029626,-11.2505 5.344703,-17.08734 1.058468,-4.86924 1.393999,-6.0524 2.150999,-10.95293 0.608414,-3.93865 1.157935,-8.01017 0.858411,-12.00877 -0.03614,-0.48244 -0.168793,-0.95273 -0.253193,-1.42909 0,0 14.561624,-10.57964 14.561624,-10.57964 z"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#b4b4b4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ id="path5141"
+ d="m 140.01197,115.31803 c 0.1617,0.0908 0.31629,0.19554 0.48509,0.27227 0.92579,0.42082 1.65377,0.62793 2.66943,0.91832 1.74975,0.50029 3.59592,0.97959 5.37921,1.3131 1.13505,0.21227 2.28219,0.35377 3.42329,0.53066 1.11577,0.10355 2.23154,0.2071 3.34732,0.31065 0,0 -4.57828,4.47192 -4.57828,4.47192 v 0 c -1.06724,-0.15418 -2.13447,-0.30835 -3.20171,-0.46253 -1.19916,-0.12589 -2.39704,-0.26468 -3.59749,-0.37769 -3.48767,-0.32831 -6.98869,-0.57826 -10.48936,-0.70532 0,0 6.5625,-6.27138 6.5625,-6.27138 z"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer8"
+ inkscape:label="Console">
+ <path
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ id="path4968"
+ d="m 67.734061,161.32424 c 0.24221,-0.0478 0.487619,-0.0816 0.72663,-0.14341 0.456385,-0.11808 0.901703,-0.27631 1.357704,-0.39587 3.475421,-0.91119 6.973141,-1.68578 10.42017,-2.71218 1.536119,-0.4574 3.055949,-0.96787 4.583919,-1.4518 5.52937,-2.04108 11.22153,-3.99216 16.293566,-7.05331 -2.555673,1.45975 -5.112306,2.91784 -7.667026,4.37926 -0.0647,0.037 0.12518,-0.12645 0.19681,-0.10596 0.0712,0.0204 0.18018,0.17316 0.10899,0.19342 -0.11155,0.0317 -0.22822,-0.0641 -0.32154,-0.13294 -0.39059,-0.28821 -0.75275,-0.61331 -1.11998,-0.93077 -0.65432,-0.56565 -1.29623,-1.1455 -1.94434,-1.71825 -4.4401,-3.83254 -8.49664,-8.07303 -12.614678,-12.24088 -3.277171,-3.31681 -2.864934,-2.89488 -5.964311,-5.98442 -0.735343,-0.71314 -1.470687,-1.42628 -2.20603,-2.13942 0,0 7.375972,-4.47497 7.375972,-4.47497 v 0 c 0.627931,0.77724 1.255863,1.55447 1.883794,2.3317 2.107093,2.4987 3.153613,3.81025 5.479013,6.24515 4.09467,4.28749 8.5765,8.19944 13.477323,11.54321 0.78825,0.46303 1.563958,0.9481 2.364743,1.38909 0.48932,0.26947 1.81853,0.77229 2.28197,1.31373 0.16311,0.19057 0.27091,0.44681 0.2828,0.69737 0.008,0.16907 -0.15214,0.30241 -0.22821,0.45362 -2.936065,1.72315 -5.872133,3.44629 -8.808216,5.16943 -4.82104,2.37114 -9.95897,4.0309 -15.011182,5.82204 -1.556392,0.49753 -3.108954,1.0072 -4.669173,1.49258 -4.715401,1.46698 -9.470829,2.81169 -14.113864,4.5027 0,0 7.835146,-6.04912 7.835146,-6.04912 z"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ id="path4972"
+ d="m 97.149238,161.04997 c 5.291942,0.48726 10.617702,0.4481 15.925982,0.40285 4.48642,-0.16973 8.98302,-0.37955 13.43983,-0.95308 0.87713,-0.11287 1.74644,-0.2798 2.61967,-0.41971 0,0 -7.14643,5.30967 -7.14643,5.30967 v 0 c -4.99254,0.22551 -9.9893,0.32077 -14.98361,0.49991 -5.97148,0.2805 -11.940902,0.60296 -17.905032,1.00872 0,0 8.04959,-5.84836 8.04959,-5.84836 z"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ id="path5010"
+ d="m 58.580469,101.50775 c -0.114684,0.58579 -0.193326,1.1798 -0.344054,1.75737 -0.898075,3.44129 -2.039916,6.81577 -2.941592,10.25612 -1.660832,6.33691 -3.090802,12.73253 -4.726128,19.07607 -1.04567,4.05621 -2.144479,8.09855 -3.216719,12.14782 -3.960141,14.46669 -7.955322,28.9237 -12.01747,43.36207 0,0 -14.393031,6.77063 -14.393031,6.77063 v 0 c 1.067671,-2.89087 2.184457,-5.76408 3.203014,-8.67262 4.048947,-11.56197 7.567704,-23.31261 10.298535,-35.25912 0.829321,-3.91926 1.720636,-7.82591 2.487956,-11.75779 1.758006,-9.00831 3.235881,-18.17665 3.936154,-27.33885 0.03391,-0.4437 0.02381,-0.88968 0.03573,-1.33451 0,0 17.677606,-9.00719 17.677606,-9.00719 z"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ id="path5018"
+ d="m 49.100022,183.08643 c 0.479862,-0.13959 0.953119,-0.30432 1.439588,-0.41878 2.771375,-0.65209 7.066428,-1.39312 9.51992,-1.73532 8.689141,-1.21191 15.11873,-1.71996 24.10098,-2.54957 5.256318,-0.48548 10.5142,-0.95388 15.771299,-1.43082 21.773291,-1.66827 10.963571,-0.91972 32.428661,-2.25281 0,0 -10.1859,7.48934 -10.1859,7.48934 v 0 c -21.2298,1.09642 -10.46343,0.36531 -32.296856,2.22606 -13.8542,1.57413 -18.001168,1.81279 -31.548014,4.24227 -7.702167,1.3813 -15.664032,3.01172 -22.971657,5.94737 -0.58592,0.23538 -1.133634,0.5565 -1.700451,0.83475 0,0 15.44243,-12.35249 15.44243,-12.35249 z"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ d="m 78.49994,107.31835 c 13.201785,0.43699 26.37503,1.57155 39.55885,2.36606 5.61651,0.33847 11.23669,0.61298 16.85503,0.91947 21.62743,1.03574 11.11806,0.60669 31.52708,1.31171 0,0 -10.80449,7.75807 -10.80449,7.75807 v 0 c -19.50764,-1.53298 -9.33751,-0.84038 -30.51413,-2.02211 -5.75749,-0.19964 -11.51239,-0.49896 -17.27247,-0.59893 -10.515024,-0.18251 -19.40967,-0.0465 -29.789277,0.5119 -4.355438,0.23433 -9.068514,0.63726 -13.449181,1.25787 -0.635957,0.0901 -1.261438,0.24293 -1.892154,0.3644 0,0 15.780742,-11.86844 15.780742,-11.86844 z"
+ inkscape:connector-curvature="0"
+ id="path5026" />
+ <path
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ id="path5036"
+ d="m 173.391,117.18602 c -4.1275,6.19983 -6.70053,13.25926 -9.76261,20.00451 -1.00774,2.21987 -2.08534,4.40737 -3.12801,6.61105 -4.15289,8.62275 -8.39098,17.20438 -12.217,25.97877 0,0 -16.58629,7.52813 -16.58629,7.52813 v 0 c 4.66429,-8.78005 9.36653,-17.5446 13.57862,-26.55471 0.94153,-2.1368 1.91587,-4.25944 2.8246,-6.4104 2.64367,-6.2575 4.84489,-12.68071 7.14397,-19.06823 0,0 18.14672,-8.08912 18.14672,-8.08912 z"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="Color"
+ style="display:inline">
+ <path
+ style="fill:url(#linearGradient1324);fill-opacity:1;stroke-width:3.89081144;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 467.3058,520.95276 c 1.6002,-6.45879 1.85059,-22.73651 0.30427,-31.24647 -4.8473,-26.67654 -24.49028,-37.34596 -40.7577,-41.40855 -19.12371,17.65581 -8.8238,12.71425 -20.34484,19.12348 -116.66018,64.89885 -210.0471,22.7807 -187.9256,-70.75632 l 8.67349,-1.70132 c 2.47028,-0.46699 6.96525,-1.83623 10.21124,-2.82033 l 4.52083,-2.15946 3.13726,2.98099 c 2.26364,1.39901 6.06458,1.10127 9.66775,1.09091 10.54698,-0.0303 22.92142,-7.2932 34.49006,-18.00372 1.95995,-1.81457 7.883,-6.84944 7.68303,-6.19823 -4.15095,13.5177 -8.48766,32.39322 -3.35528,37.67786 26.21867,9.66816 26.57593,-4.91387 34.75129,-15.60505 23.48189,-29.47515 57.16242,-48.52534 89.71138,-52.54878 9.55657,-1.1813 24.46548,-0.028 33.12778,2.1546 10.31041,2.59785 25.03971,9.52958 32.91332,15.17854 36.95269,26.51191 49.10988,91.02329 46.15935,152.97477 l -1.45141,6.75404 c -20.21292,5.29715 -36.90058,8.7104 -54.80239,8.94149 -1.90012,-0.0134 -7.23067,-2.20004 -6.71383,-4.42845 z"
+ id="path4947"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="cscsccccccssccssscccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:#e3a100;fill-opacity:1;stroke-width:3.89081144;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 222.21142,497.45241 c -23.11471,-32.92976 -22.87439,-57.62535 -19.90183,-86.17581 2.38831,-5.49039 10.45621,-7.6916 15.01225,-10.17633 -0.49526,11.59151 -2.40024,26.44853 -0.38864,38.44543 2.79776,15.80123 11.56918,29.8234 21.79441,40.07812 21.07796,21.13871 53.95919,24.52328 97.06434,16.10958 40.52637,-3.52209 69.23847,-27.61956 91.06042,-47.43566 5.85893,3.05301 17.49502,1.49118 15.71764,4.71168 -29.38959,53.2521 -107.46245,67.25417 -123.01008,69.27824 -30.36686,2.32132 -61.97002,1.00292 -97.34851,-24.83525 z"
+ id="path4989"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="ccccsccscc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:#e3a100;fill-opacity:1;stroke-width:3.89081144;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 442.86524,523.16565 c 8.64377,-39.97677 -2.79006,-46.4585 -16.23066,-51.16531 4.19064,-4.97942 7.68865,-17.16441 10.16047,-22.17325 6.98434,3.43351 21.80709,13.73325 25.06451,20.0764 5.94537,17.58779 6.87698,40.15549 4.76409,55.57066 -9.60591,-2.91473 -14.16262,-2.4811 -23.75841,-2.3085 z"
+ id="path4991"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="cccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:#8f5127;fill-opacity:1;stroke-width:1.94540572;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 442.86524,523.16565 c 28.14463,4.68 60.16373,0.72567 85.95678,-6.72593 -15.65578,25.62582 -23.18378,36.71361 -36.82311,58.95197 -7.16766,1.26319 -15.12825,1.22807 -22.16194,1.32975 -5.42634,-11.15826 -18.63586,-41.51628 -26.97173,-53.55579 z"
+ id="path4993"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="ccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#000000;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 125.08126,152.89281 0.6812,3.00787 0.99051,1.04888 2.63062,-4.20618 z"
+ id="path4995-7-9"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#c36f35;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 133.09308,138.61291 -5.30511,13.82027 0.85432,0.50413 8.07809,-15.08411 z"
+ id="path4995"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:none;fill:#4d2d14;fill-opacity:0.43627451;stroke-width:0.45687732;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 117.17476,138.04341 c 7.4466,0.97558 15.91832,0.15127 22.74273,-1.40207 -4.14225,5.34188 2.50629,-2.57387 -1.10245,2.06187 -9.56935,2.66058 -13.72611,1.81942 -20.34961,1.42371 -1.43572,-2.32602 0.91487,0.42621 -1.29067,-2.08351 z"
+ id="path4993-2"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="display:inline;fill:#4d4d4d;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 127.78797,152.43318 -1.95742,3.4675 0.78623,0.6233 2.52848,-4.22321 z"
+ id="path4995-7"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:url(#linearGradient1316);fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 251.8142,358.97668 c 9.74858,-2.49638 23.63845,-4.86903 28.1858,-2.66235 -7.22291,15.20864 -61.08745,29.50158 -89.46428,30.22321 18.17975,-16.07587 46.42904,-23.15219 61.27848,-27.56086 z"
+ id="path5044"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 153.57143,360.73398 c 1.12123,-0.67607 5.74983,0.423 7.53255,0.2344 5.59772,-1.10471 9.91884,17.77361 15.50317,18.94414 11.63069,-2.58085 41.02994,-17.36501 55.79884,-25.48544 17.2548,-7.36858 33.37618,-24.02702 51.87972,-25.12168 5.03044,5.52924 -0.25599,21.37994 -4.28571,27.00893 -7.5741,-2.07773 -18.01813,-0.95456 -23.62152,0.22671 -19.32799,11.53065 -54.31927,12.60016 -65.84276,29.9965 -16.23344,-4.1414 -35.46899,-9.26293 -36.96429,-25.80356 z"
+ id="path5046"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="ccccccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 176.60715,379.91252 c -1.69036,-1.60312 -10.76126,-14.63128 -12.12075,-19.14863 16.31706,-4.189 31.52837,-12.77517 46.59772,-19.73913 23.79165,-10.78361 30.11745,-12.47321 41.7589,-15.99656 7.43877,-2.15494 15.9562,-4.63991 23.58555,-4.65137 1.13158,0.97732 6.88981,7.85492 7.85714,8.92857 -9.30626,4.27034 -10.16848,-0.72593 -19.29227,4.09504 -11.62268,6.18548 -48.78527,32.26899 -59.57567,37.59103 -5.49523,2.2108 -28.81062,8.92105 -28.81062,8.92105 z"
+ id="path5048"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="ccccccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:url(#linearGradient1288);fill-opacity:1;fill-rule:nonzero;stroke-width:0.25736097"
+ d="m 185.02104,226.82904 c 15.68077,-13.6058 45.4929,-13.49078 66.37362,-10.19104 44.4703,18.89436 29.04637,70.2615 28.09282,83.70293 -35.97544,5.41916 -56.72632,9.48344 -82.47512,14.34571 -19.8524,-45.62958 -23.07786,-66.85674 -11.99132,-87.8576 z"
+ id="path874"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="ccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:url(#linearGradient1332);fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 119.65337,344.87918 c 25.00001,-10.64117 57.24303,-17.7072 77.35899,-30.19254 27.84595,-3.42136 58.0513,-9.95265 82.47512,-14.34571 27.49506,9.59326 32.92464,19.25336 33.28967,30.84984 -5.92411,30.0594 -12.9247,49.58546 -56.29282,61.97658 -7.57451,-0.0754 -12.81073,-3.12067 -19.01767,-1.0241 -8.67279,4.28068 -17.34538,0.73538 -18.88473,4.52165 -48.78674,47.46912 -123.116579,4.10706 -98.92856,-51.78572 z m 87.12152,43.77958 c 29.2346,-2.84886 89.78686,-23.55709 81.10226,-55.00979 -3.02153,-6.94584 -5.26189,-13.61643 -11.44858,-13.27214 -36.70857,3.96521 -81.22603,27.91262 -113.39285,38.92857 -14.39969,2.97306 -6.69739,8.17854 -4.37654,18.30724 16.24135,10.05515 32.95154,12.19462 48.11571,11.04612 z"
+ id="path5050"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="cccccccccccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:url(#linearGradient1296);fill-opacity:1;stroke-width:0.25736097"
+ d="m 104.54016,291.9971 c 1.9503,-37.94092 15.96479,-68.7271 65.57405,-75.86029 29.60578,-0.82449 24.1206,70.09595 26.89815,98.54983 -17.68507,6.74322 -58.4066,19.7808 -75.99175,29.55514 -9.6311,-4.66638 -13.8777,-34.27929 -16.48045,-52.24468 z"
+ id="path870"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="ccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26706785px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 47.846716,61.809749 c 0.641134,2.174906 0.352672,8.284835 0.03144,12.821736 0,0 1.363305,-0.347657 2.719698,-0.464587 -0.247082,-6.645292 -0.854615,-10.348714 -2.75114,-12.357149 z"
+ id="path1639"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.24577013px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 70.896551,64.043568 c 0.922411,3.519263 0.445253,6.857512 0.184606,8.791221 0,0 1.283004,-0.06907 2.101517,0.351881 0.354345,-4.76371 -0.221413,-6.63913 -2.286123,-9.143102 z"
+ id="path1639-2"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:#d8d8d8;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 28.71811,69.687267 -0.342271,2.234377 c 5.144939,-3.274052 21.184763,-5.023947 21.294357,-9.848699 l -1.940411,-2.438101 z"
+ id="path1615"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:#dc8800;fill-opacity:1;stroke:none;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 53.527745,108.81693 -0.586356,5.92944 c 5.392724,-7.70389 9.978939,-6.28632 9.978939,-6.28632 9.533565,3.27415 16.592355,-12.313097 16.592355,-12.313097 0,0 -9.853358,11.369697 -15.486828,7.036257 l -6.19272,1.76771 z"
+ id="path1388"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:#dc8800;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 79.779953,116.25898 c -1.157588,2.21304 -0.627005,-8.04772 -0.627005,-8.04772 l 3.818884,-2.7e-4"
+ id="path1390"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:#dc8800;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 107.52122,118.39397 c 0.86356,-1.00632 -2.7403,0.25143 1.69445,3.71458 10.14011,-1.69787 13.73278,6.48984 14.82319,10.93604 -1.25972,-12.52919 -7.22289,-15.12727 -16.51764,-14.65062 z"
+ id="path1407"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#e2e2e2;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 44.511423,97.442747 c -0.447241,-0.42416 -0.631365,-0.79542 -0.991063,-1.990633 4.317222,-1.10834 8.341881,-3.380097 12.32898,-5.222645 6.294874,-2.853163 7.968575,-3.300203 11.048709,-4.232423 1.968175,-0.570161 4.221745,-1.227643 6.240343,-1.230675 0.299398,0.258583 0.500012,0.986875 0.755952,1.270945 -7.54174,-0.0073 -12.539756,3.996575 -19.953828,7.567726 -1.453946,0.58494 -9.429093,3.837705 -9.429093,3.837705 z"
+ id="path5048-9"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#d8d8d8;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 72.509676,62.519441 1.708249,4.483335 C 70.830052,65.38237 50.489668,68.064246 50.599262,63.239494 l -0.929066,-1.166549 z"
+ id="path1615-0"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#000000;fill-opacity:0.11499999;stroke:none;stroke-width:0.26706785px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 52.126186,83.260839 2.194829,-0.285523 c 0.0817,-12.623535 -2.321322,-14.35395 -3.093367,-14.708739 1.026992,6.733567 1.180981,10.171089 0.898538,14.994262 z"
+ id="path1639-1"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ </g>
+ <g
+ inkscape:label="Stroke"
+ inkscape:groupmode="layer"
+ id="layer1"
+ style="display:inline">
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 79.779953,116.25898 C 93.410697,79.108514 145.79742,75.971158 139.91749,136.64134"
+ id="path3717"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 53.237322,108.51043 c -4.896999,46.88708 56.107608,28.19645 60.290568,15.17697"
+ id="path3713"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 79.512683,96.146953 c 0,0 -8.282108,15.993927 4.27956,11.717607"
+ id="path3715"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 113.32417,125.08477 c 7.41673,1.47 3.85059,13.33614 3.85059,13.33614"
+ id="path3719"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 117.17476,138.42091 c 0,0 10.34811,2.59697 22.74273,-1.77957 l -13.03173,21.25588 z"
+ id="path3772"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 124.31103,152.59088 c 0,0 3.69051,0.33268 5.86368,-0.35183 l -3.28895,5.65817 z"
+ id="path3772-5"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ inkscape:connector-curvature="0"
+ style="display:inline;fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 60.100607,56.203547 c -0.312748,-0.0035 -0.631807,-0.0016 -0.957564,0.0067 -5.158045,0.164457 -8.343056,1.632934 -10.18956,3.804936 4.335336,6.683277 3.160014,23.257494 3.160014,23.257494 L 73.93542,79.477064 c 0,0 5.86822,-23.052701 -13.834813,-23.273515 z"
+ id="path3864-6-6"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:none;fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 52.126185,83.26084 21.821544,-3.795636 c 0,0 6.056174,-23.790116 -14.792335,-23.266371 -22.87896,0.729464 -7.029209,27.062007 -7.029209,27.062007 z"
+ id="path3864-6"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 31.658288,91.249283 52.126185,83.26084 c 0,0 2.143572,-30.192966 -10.011484,-25.511483 -12.41316,3.461278 -14.830305,12.210667 -14.246716,19.899712 0.55214,7.274676 3.790303,13.600214 3.790303,13.600214 z"
+ id="path3864"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccsc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 31.658288,91.249283 c -7.46503,13.890627 12.000741,27.781257 26.174848,13.701637"
+ id="path3804"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 52.600301,104.22265 c 3.779762,1.98437 11.425554,-1.03944 11.425554,-1.03944"
+ id="path3806"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 73.947729,79.465204 c 21.733631,3.96875 -1.836971,30.332926 -9.921874,23.718006"
+ id="path3808"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 36.852678,93.459821 c 4.695137,7.162019 23.048516,-8.41416 36.285714,-8.693452 C 87.403173,96.673874 45.392511,109.8083 40.985075,97.215376 40.795373,96.673361 40.675333,96.083684 40.632441,95.444197"
+ id="path3810"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccsc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <ellipse
+ id="path3721-3"
+ cx="92.276497"
+ cy="115.87661"
+ rx="4.4040084"
+ ry="1.8882898"
+ style="fill:#ffe373;fill-opacity:1;stroke-width:1.41250539;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ transform="matrix(0.97437654,-0.22492299,0.16269712,0.98667606,0,0)"
+ inkscape:transform-center-x="7.6232607"
+ inkscape:transform-center-y="1.6495751"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <ellipse
+ id="path3721-3-6"
+ cx="114.30614"
+ cy="84.435722"
+ rx="2.9982629"
+ ry="1.4208385"
+ style="fill:#ffefad;fill-opacity:1;stroke-width:0.98462951;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ transform="matrix(0.99711986,0.07584183,0.0314437,0.99950552,0,0)"
+ inkscape:transform-center-x="5.300598"
+ inkscape:transform-center-y="-0.45710738"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <ellipse
+ id="path3721"
+ cx="38.195225"
+ cy="138.58012"
+ rx="5.06603"
+ ry="2.1960781"
+ style="fill:#ffdc50;fill-opacity:1;stroke-width:1.2965169;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ transform="matrix(0.81464891,-0.57995444,0.4886608,0.87247385,0,0)"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 107.52122,118.39397 c 20.98066,-2.00451 15.93962,20.63773 15.93962,20.63773"
+ id="path3719-5"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 50.412574,102.27139 C 50.034598,99.342073 73.138392,91.534504 74.083332,94.274831"
+ id="path3842"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 58.620767,100.38579 c -0.258061,-1.760874 6.292318,-5.241102 6.292318,-5.241102"
+ id="path3844"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 43.136534,95.066219 c -0.283482,2.078869 3.590774,5.452301 3.590774,5.452301 8.646205,-0.0945 20.12723,-13.342553 28.489953,-13.3898"
+ id="path3862"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#000000;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 46.097823,85.643117 c 0.434158,-0.790147 4.010449,-1.580293 6.015674,-2.37044 l -0.08909,-7.468753 c -12.258275,0.240298 -5.926582,9.839193 -5.926582,9.839193 z"
+ id="path3864-2"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#000000;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 66.984677,80.331801 6.963052,-0.866597 c 0,0 0.639352,-1.780937 0.451021,-4.594736 -7.730283,-1.939111 -7.618353,1.477868 -7.414073,5.461333 z"
+ id="path3864-6-1"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 57.833136,104.95092 c -6.064506,35.90054 35.507307,30.92767 52.239184,16.30613"
+ id="path3713-3"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <ellipse
+ id="path3721-3-6-2"
+ cx="129.82088"
+ cy="67.603104"
+ rx="1.3405895"
+ ry="0.82212281"
+ style="display:inline;fill:#ffefad;fill-opacity:1;stroke-width:0.39055166;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ transform="matrix(0.97841786,0.20663611,-0.08053839,0.99675151,0,0)"
+ inkscape:transform-center-x="2.3232657"
+ inkscape:transform-center-y="-0.50995364"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <ellipse
+ id="path3721-3-6-2-0"
+ cx="147.04755"
+ cy="32.611526"
+ rx="0.80951613"
+ ry="0.52924711"
+ style="display:inline;fill:#ffefad;fill-opacity:1;stroke-width:0.24350296;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ transform="matrix(0.900137,0.43560691,-0.26464483,0.96434595,0,0)"
+ inkscape:transform-center-x="1.2882739"
+ inkscape:transform-center-y="-0.63682807"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer6"
+ inkscape:label="Eyebrow fill">
+ <path
+ style="display:inline;fill:url(#linearGradient1340);fill-opacity:1;stroke-width:0.04814932"
+ d="m 49.240248,61.44456 c -1.39884,-2.158138 -3.15151,-8.001726 -0.16975,-8.153779 7.31775,-0.848543 36.0756,-0.38685 35.55185,11.394969 -11.11794,-4.758679 -23.6343,-0.47105 -35.3821,-3.24119 z"
+ id="path868"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:url(#linearGradient1348);fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 20.840178,75.55899 c -0.44115,-3.0454 -0.69459,-6.18321 1.61155,-10.53351 7.12672,-10.167588 22.91987,-14.121603 24.82536,-14.20119 1.87839,-0.07846 2.09827,4.440478 2.10612,6.047057 -3.72626,5.252163 -13.70165,9.287043 -17.89446,11.538573 -4.25531,1.93781 -7.3585,5.17563 -10.64857,7.14907 z"
+ id="path5084"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccsccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer5"
+ inkscape:label="Eyebrow stroke">
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 49.070488,53.290779 c -2.96717,5.655134 -0.10364,8.070692 -0.10364,8.070692 4.15993,3.345242 22.15588,-1.595344 35.6555,3.324276 C 82.638318,50.39459 49.070498,53.290779 49.070498,53.290779 Z"
+ id="path3926"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#000000;fill-opacity:0.26960784;stroke:none;stroke-width:0.24370429px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 39.654497,64.570006 5.029341,-3.765877 -22.51242,4.187357 -1.577119,3.858302 c 7.242969,-2.61276 13.503948,-3.254522 19.060198,-4.279782 z"
+ id="path1639-2-3-7-9-2-2-8"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 47.415148,50.689191 c 1.74176,2.693482 1.96806,6.182156 1.96806,6.182156 -0.95096,5.405647 -26.01936,13.322927 -28.54303,18.687647 -4.50646,-13.686736 15.45263,-22.971986 26.57497,-24.869803 z"
+ id="path3928"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#000000;fill-opacity:0.26960784;stroke:none;stroke-width:0.20224327px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 53.013585,62.51189 c -3.538802,-0.463021 -5.2838,-2.877046 -5.2838,-2.877046 19.546094,-3.73724 24.96401,-4.010589 28.800468,-4.043662 l 4.194995,3.4945 C 79.342059,57.465109 63.190101,59.667618 53.013585,62.51189 Z"
+ id="path1639-2-3-7-9-2-2-8-9"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer3"
+ inkscape:label="Cap color"
+ style="display:inline">
+ <path
+ style="display:inline;fill:url(#linearGradient1458);fill-opacity:1;stroke-width:0.15441659;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 68.695517,55.448172 c 8.831601,-3.62349 14.940011,-6.507575 15.346522,-7.444229 -0.735039,-2.86466 -2.35349,-8.83412 -3.015369,-11.460565 3.02146,-0.185701 10.21016,0.06165 13.567619,0.622302 4.821907,1.257457 14.508341,7.940297 13.651401,11.84518 -4.57522,5.328539 -32.871161,6.803043 -39.550173,6.437312 z"
+ id="path900"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:url(#linearGradient1356);fill-opacity:1;stroke-width:0.43675604;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 19.700578,35.768683 c 9.781034,-8.692226 15.60081,-13.163897 31.51203,-14.028922 -17.630099,10.25988 -13.417039,16.97125 -8.849059,38.621436 -6.30278,1.594032 -17.719045,5.538221 -20.192132,4.630287 -5.107566,-7.476238 -9.714321,-19.113879 -2.470839,-29.222801 z"
+ id="path902"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 18.557038,59.502611 c 0,0 3.219656,0.65801 6.030537,-0.337638 -2.501152,-14.960093 -0.658083,-13.248892 -7.675688,-13.68057 -0.303328,3.81035 -1.337264,4.73961 1.645151,14.018208 z"
+ id="path4735-0"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:transform-center-x="-18.798168"
+ inkscape:transform-center-y="0.27207299"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:url(#linearGradient1372);fill-opacity:1;stroke-width:1.16724348;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 194.28741,82.166031 c 29.93567,4.276159 80.20628,10.658075 98.18436,31.249759 13.77077,24.70092 19.83249,34.46611 25.81026,68.11886 0,0 -22.58589,14.74217 -64.07804,29.49134 -8.54237,-76.30944 -9.51304,-106.42264 -59.91658,-128.859959 z"
+ id="path1204"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="ccscc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:url(#linearGradient1364);fill-opacity:1;stroke-width:1.16724348;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 160.1142,228.13682 c -4.38774,-22.93332 -11.98633,-54.82311 -12.4009,-61.35884 -1.78389,-28.1231 -2.02942,-37.00485 7.64969,-52.37296 4.36959,-6.93787 16.09574,-16.306912 36.85569,-31.207297 13.21333,5.745177 36.0957,19.287177 43.71385,31.669767 10.98521,28.76738 12.15114,38.70668 18.27146,96.1585 -22.59781,5.14186 -64.18953,13.29088 -94.08979,17.11083 z"
+ id="path1206"
+ inkscape:connector-curvature="0"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="csscccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="fill:#5a0000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
+ d="m 68.695517,55.448172 14.211632,-0.513949 c 1.81901,-3.175 1.685795,-7.829223 1.685795,-7.829223 0,0 -8.667351,5.642541 -15.897427,8.343172 z"
+ id="path1546"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:none;stroke:#4b0000;stroke-width:0.30000001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 79.110726,53.589939 C 87.538919,52.911998 103.17295,50.34183 105.29574,48.702777 106.53231,45.065678 97.134533,39.676517 93.478411,38.752361 89.115094,38.075399 87.764768,37.516888 82.71851,38.087157"
+ id="path4737-6"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:none;stroke:#4b0000;stroke-width:0.30000001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 80.878057,52.313588 C 88.657067,51.856707 100.24565,49.588863 103.909,48.202788 104.77135,45.378462 96.635808,40.242947 93.387496,39.42187 88.64064,38.574807 87.586525,38.478769 82.859471,38.696692"
+ id="path4737-6-1"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:none;fill:#030303;fill-opacity:0.14215686;stroke:none;stroke-width:0.24577014px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 63.243723,31.827346 c 1.801681,6.586852 4.041203,20.108472 4.014416,24.006612 l 4.185514,-1.603149 C 71.330278,48.6252 66.434289,35.048951 63.243723,31.827346 Z"
+ id="path1639-2-3"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ style="display:inline;fill:none;stroke:#4b0000;stroke-width:0.30000001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 82.391172,51.058262 c 3.041317,0.08311 14.575836,-2.053389 20.374078,-3.464784 0.36972,-1.929041 -6.660707,-6.35996 -9.818754,-7.537532 -5.640231,-1.126134 -5.064112,-0.592996 -9.964517,-0.713341"
+ id="path4737-6-1-8"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#ffffff;fill-opacity:0.14215686;stroke:none;stroke-width:0.25908363px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 69.250851,26.74182 c 6.009721,0.865738 11.628592,9.867987 13.005251,20.931395 l -5.312421,2.280276 C 76.528434,41.71662 74.603365,29.894258 69.250851,26.74182 Z"
+ id="path1639-2-3-7"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#ffffff;fill-opacity:0.14215686;stroke:none;stroke-width:0.25908363px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 52.589995,24.367778 c 4.201257,3.328935 6.908998,4.486034 9.258464,9.041462 1.887608,3.659922 3.166593,12.731256 3.208064,21.029577 l -5.166046,1.2055 C 59.755862,45.162382 60.374661,35.097307 52.589995,24.367778 Z"
+ id="path1639-2-3-7-9"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="csccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#000000;fill-opacity:0.13333333;stroke:none;stroke-width:0.24370429px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 48.879814,22.598902 C 28.647029,33.472366 32.532352,40.885322 36.086034,61.620645 l 6.277515,-1.259448 c -2.314068,-13.560173 -4.15874,-21.042588 -3.314327,-25.836741 0.716943,-4.070443 4.365152,-8.051477 9.830592,-11.925554 z"
+ id="path1639-2-3-7-9-2"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccsc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:none;fill:#000000;fill-opacity:0.14215686;stroke:none;stroke-width:0.24370429px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 40.399118,59.612842 -1.006467,-5.866007 -18.49406,6.254414 2.027829,3.229916 z"
+ id="path1639-2-3-7-9-2-2"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="display:none;fill:#000000;fill-opacity:0.14215686;stroke:none;stroke-width:0.24370429px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 64.311492,54.758198 c -0.07016,-3.706694 -0.236706,-6.433794 -0.236706,-6.433794 l -21.503696,4.348588 1.366371,6.206478 z"
+ id="path1639-2-3-7-9-2-2-7"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="display:none;fill:#000000;fill-opacity:0.14215686;stroke:none;stroke-width:0.24370429px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 82.256102,47.673215 c -0.15201,-1.572714 -0.633037,-3.662902 -0.633037,-3.662902 l -14.883925,3.821034 0.220453,6.089547 c 0,0 10.34789,-3.975671 15.296509,-6.247679 z"
+ id="path1639-2-3-7-9-2-2-7-3"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="display:inline;fill:#ffffff;fill-opacity:0.14215686;stroke:none;stroke-width:0.24370429px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 26.742166,31.078961 c -5.620633,4.346584 -7.615726,9.067907 -8.174434,12.634089 2.338608,-0.02339 3.872033,0.211483 4.947793,1.263851 0.124181,-2.611163 0.287762,-9.449684 3.226641,-13.89794 z"
+ id="path1639-2-3-7-9-2-6"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#ffffff;fill-opacity:0.04901961;stroke:none;stroke-width:0.24577016px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 47.335063,24.86211 C 33.989349,33.301046 39.810648,46.475039 42.363548,60.3612 l 3.90779,-0.747696 C 43.1814,45.871958 37.693337,33.706111 47.335063,24.86211 Z"
+ id="path1639-2-3-1"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#030303;fill-opacity:0.34803922;stroke:none;stroke-width:0.24577017px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 18.557038,59.502611 c 0.677202,5.263935 2.59856,4.204416 3.89469,5.522869 l 8.604144,-1.938321 C 27.622774,62.04054 20.829782,62.262365 18.557038,59.502611 Z"
+ id="path1639-2-3-1-2"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:#ffba6c;fill-opacity:0.16176471;stroke:none;stroke-width:0.25908363px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 85.181281,37.264008 c 5.049217,0.08214 4.104388,0.136515 9.157879,0.950869 1.417528,0.22843 4.782127,2.434611 7.85587,4.3886 2.61656,1.663358 3.72866,3.531301 4.28372,5.195956 -0.0802,0.323582 -0.25469,1.222834 -0.72002,1.50348 -0.62166,0.374933 -1.69885,0.542929 -2.44891,0.826014 -3.41268,1.288 -8.273894,2.02374 -8.273894,2.02374 0.89437,-6.085352 -4.502131,-11.736221 -9.854645,-14.888659 z"
+ id="path1639-2-3-7-93"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="csscsscc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer4"
+ inkscape:label="Cap stroke">
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 51.405209,21.739762 C 36.723039,22.814108 30.421259,25.5501 22.20512,33.252698 14.407773,40.562682 13.84721,56.16001 22.17142,64.991486 h -2e-6 c 5.666271,-1.205637 45.086721,-9.157528 45.086721,-9.157528"
+ id="path4735"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="csccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 67.258139,55.833958 c 0,0 12.22059,-4.164255 16.7839,-7.830015 -4.26409,-16.150828 -4.00368,-23.900082 -32.82943,-26.26418 15.24158,8.493997 13.42041,12.729126 16.04553,34.094195 z"
+ id="path4735-7"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <ellipse
+ style="display:inline;fill:#000000;stroke-width:1.58873022;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ id="path4772"
+ cx="50.137604"
+ cy="22.150518"
+ rx="3.1778214"
+ ry="1.7545754"
+ transform="matrix(0.9999861,-0.00527254,0.03028528,0.9995413,0,0)"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <ellipse
+ style="display:inline;fill:#000000;stroke-width:0.67251223;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ id="path4772-3"
+ cx="-24.066288"
+ cy="-35.526993"
+ rx="1.110212"
+ ry="0.89990294"
+ transform="matrix(-0.99529388,0.09690251,-0.22035155,-0.97542052,0,0)"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <ellipse
+ style="display:inline;fill:#000000;stroke-width:0.72235405;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ id="path4772-3-6"
+ cx="-18.0144"
+ cy="-51.72073"
+ rx="1.1887667"
+ ry="0.96962708"
+ transform="matrix(-0.93021237,0.36702173,-0.61230445,-0.79062207,0,0)"
+ inkscape:transform-center-x="0.25772509"
+ inkscape:transform-center-y="-1.03984"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <ellipse
+ style="display:inline;fill:#000000;stroke-width:0.67251223;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
+ id="path4772-3-0"
+ cx="-25.902897"
+ cy="-65.254128"
+ rx="1.110212"
+ ry="0.89990294"
+ transform="matrix(-0.78280101,0.62227211,-0.71509636,-0.69902589,0,0)"
+ inkscape:transform-center-x="-0.61843122"
+ inkscape:transform-center-y="-0.68944426"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 42.363549,60.361199 c -3.74513,-21.789896 -8.85523,-27.726488 8.84906,-38.621436"
+ id="path938"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 67.258139,55.833958 c 10.443193,0.128338 38.647991,-2.876116 41.104371,-7.038332 0.252,-3.952242 -8.766093,-10.36556 -13.768221,-11.629946 -3.434826,-0.591137 -6.038168,-0.991806 -8.625235,-0.985821 -1.691753,0.0039 -3.333794,0.145912 -5.089611,0.429948"
+ id="path4737"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ <ellipse
+ style="fill:#030303;fill-opacity:0.14215686;fill-rule:nonzero;stroke:none;stroke-width:0.30000001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.58431373"
+ id="path1732"
+ cx="66.930969"
+ cy="19.734592"
+ rx="0.046772167"
+ ry="0.18708867"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer7"
+ inkscape:label="Movement">
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 108.83827,45.451835 c -0.0152,-0.182139 1.7763,1.670283 1.7198,3.505727 -0.25209,1.511495 -4.10105,3.175002 -4.10105,3.175002"
+ id="path4661"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 111.04136,47.104743 c -0.0152,-0.182139 0.85989,0.476316 0.91836,1.821016 -0.10524,1.081606 -1.49763,1.875814 -1.49763,1.875814"
+ id="path4661-6"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 85.747089,60.803045 c 1.21608,3.461139 0.43368,5.624983 0.43368,5.624983"
+ id="path4680"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 87.203879,63.050442 c 0.42233,2.402806 -0.22778,3.706754 -0.22778,3.706754"
+ id="path4680-7"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 50.481119,19.605349 c -2.31511,-0.529167 -3.72071,1.190625 -3.72071,1.190625"
+ id="path4699"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 49.058979,18.828134 c -1.47174,0.04961 -2.29857,1.091407 -2.29857,1.091407"
+ id="path4699-5"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 20.136719,31.858865 c 0,0 -4.0349,2.811197 -4.46485,6.250781"
+ id="path4716"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 16.476909,34.104282 c 0,0 -2.11667,1.521353 -2.54662,4.960937"
+ id="path4716-3"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 33.330169,99.803583 c 0.22194,6.247707 6.44143,5.519127 6.44143,5.519127"
+ id="path4733"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 71.794729,82.510737 c 3.52923,-1.822104 4.9285,2.921592 4.9285,2.921592"
+ id="path4733-5"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:1.20000005;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 134.77877,116.02006 c 1.87089,5.33203 0.90599,18.40975 0.90599,18.40975"
+ id="path4750"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 80.990879,80.046106 c 3.16033,1.692963 3.175,3.96875 3.175,3.96875"
+ id="path4752"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 83.142469,80.058266 c 2.13134,0.710748 2.42664,2.472039 2.42664,2.472039"
+ id="path4752-6"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 98.141722,90.918245 c 0,0 3.414358,-3.133735 8.044798,-3.414371"
+ id="path4771"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 98.770099,88.73543 c 1.455201,-1.289844 4.468481,-2.320123 6.205051,-2.154272"
+ id="path4771-2"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 55.144813,130.03144 c 0,0 1.446628,5.254 7.325478,6.7417"
+ id="path4788"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 53.859747,130.99414 c 0,0 1.122475,4.0616 5.546141,5.54126"
+ id="path4788-9"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 53.278758,132.6978 c 0,0 0.516063,2.08517 3.254639,3.44261"
+ id="path4788-9-1"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 27.936689,96.109952 c -1.32956,2.199672 0.0736,5.986848 0.0736,5.986848"
+ id="path4820"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 27.303119,96.345431 c -0.93269,1.736649 -0.48864,3.936309 -0.48864,3.936309"
+ id="path4820-2"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 115.59809,140.41424 c 0.84661,2.40572 2.97365,5.61185 2.97365,5.61185"
+ id="path5038"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 115.66473,142.77165 c 0.797,1.87655 2.21297,3.62748 2.21297,3.62748"
+ id="path5038-7"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cc" />
+ </g>
+</svg>
diff --git a/man/diary.1 b/man/diary.1
@@ -0,0 +1,203 @@
+.TH DIARY 1
+.SH NAME
+diary \- Text-based journaling program
+
+.SH SYNOPSIS
+.B diary
+[\fIOPTION\fR]... [\fIDIRECTORY\fR]...
+.br
+
+.SH DESCRIPTION
+.B diary
+is a text-based program for managing journal entries.
+
+.SH OPTIONS
+.TP
+\fB\-v\fR, \fB\-\-version\fR
+Print diary version
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+Show diary help text
+.TP
+\fB\-d\fR, \fB\-\-dir\fR=\fI\,DIARY_DIR\/\fR
+Diary storage directory DIARY_DIR
+.TP
+\fB\-e\fR, \fB\-\-editor\fR=\fI\,EDITOR\/\fR
+EDITOR is the text editor used for opening the journal files.
+.TP
+\fB\-f\fR, \fB\-\-fmt\fR=\fI\,FMT\/\fR
+FMT is a custom date and file format. Change with care, because the diary
+reads and writes to files with file name FMT. The new FMT is only
+applied to newly saved entries. Existing entries with the old FMT are not
+automatically migrated to the new FMT and do not show up with a new FMT
+specifier. Consequently, a change in FMT shows an empty diary at first.
+Rename all files in the DIARY_DIR to migrate to a new FMT.
+.TP
+\fB\-r\fR, \fB\-\-range\fR=\fI\,RANGE\/\fR
+RANGE is the number of years to show before/after todays date. Defaults to 1 year.
+.TP
+\fB\-f\fR, \fB\-\-weekday\fR=\fI\,DAY\/\fR
+First day of the week. DAY is an integer in range (0..6), interpreted as 0 or 7 = Sun,
+1 = Mon, ..., 6 = Sat. If glibc is installed, the first day of the week is derived
+from the current locale setting ('$LANG', see man locale). Without glibc, the
+first weekday defaults to 1 (Monday), unless specified otherwise with this option.
+
+.SH NAVIGATION
+Navigation is done using the following vim-inspired keyboard shortcuts:
+
+.TS
+tab(|);
+l l.
+Key(s) | Action
+====== | ======
+e, Enter | edit current entry
+d, x | delete current entry
+s | sync current entry with CalDAV server
+
+t | jump to today
+f | jump to or find specific day
+
+j, down | go forward by 1 week
+k, up | go backward by 1 week
+h, left | go left by 1 day
+l, right | go right by 1 day
+J | go forward by 1 month
+K | go backward by 1 month
+
+N | go to the previous journal entry
+n | go to the next journal entry
+g | go to start of journal
+G | go to end of journal
+
+q | quit the program
+.TE
+
+.SH ENVIRONMENT
+
+.IP DIARY_DIR
+If this variable is set to a directory that can be opened,
+.B diary
+will use it to store diary files. Diary files are simple text files named
+after their date, formatted according to FMT (see '-f'/'--fmt' options and
+'fmt' config key). The format defaults to "%Y-%m-%d", which is "YYYY-MM-DD"
+(see man strftime). All other files different from FMT are ignored.
+
+.IP EDITOR
+The program used to edit journal entries.
+
+.IP LANG
+The default locale used to display the first day of the week.
+
+.SH ARGUMENTS
+
+If the argument \fIDIRECTORY\fR is given, diary files are read from and
+stored to that directory, ignoring the DIARY_DIR environment variable,
+any '-d'/'--dir' options or the 'dir' value set in the config file.
+
+.SH FILES
+.TP
+.I ${PREFIX:-/usr/local/bin}/diary
+The diary binary
+.TP
+.I ${XDG_CONFIG_HOME:-~/.config}/diary/diary.cfg
+An optional diary configuration file
+
+.SH CONFIGURATION FILE
+The diary.cfg configuration file can optionally be used to persist diary
+configuration. Use the '#' or ';' characters to comment lines.
+
+Create default config location:
+
+.nf
+ mkdir -p ${XDG_CONFIG_HOME:-~/.config}/diary
+.fi
+
+Install an example config file with defaults:
+
+.in 0
+.nf
+tee ${XDG_CONFIG_HOME:-~/.config}/diary/diary.cfg <<EOF
+# Path that holds the journal text files
+#dir = ~/diary
+# Number of years to show before/after todays date
+range = 1
+# 0 = Sunday, 1 = Monday, ..., 6 = Saturday
+weekday = 1
+# Date and file format, change with care
+fmt = %Y-%m-%d
+# Editor to open journal files with
+editor =
+# Google calendar name for CalDAV sync
+#google_calendar =
+# Google OAuth2 clientid and secretid
+#google_clientid =
+#google_secretid =
+# Google OAuth2 tokenfile
+#google_tokenfile = ~/.diary-token
+EOF
+.fi
+
+.SH PRECEDENCE RULES
+
+The default variables, for instance, for the configuration variables 'editor', 'dir' and 'weekday', are populated with values in the following order:
+
+.TP
+1.
+No default for 'DIARY_DIR'. Defaults for 'range', 'weekday', 'fmt' and 'editor' are provided in 'diary.h'. If 'EDITOR' is unset and no editor is provided in the config file or via the '-e' option, the
+.B
+diary
+works read-only. Journal files cannot be opened. If 'DIARY_DIR' is not provided, the
+.B
+diary
+won't open.
+.TP
+2.
+.B
+Config file
+(empty default for 'editor', no default for 'dir')
+.TP
+3.
+.B
+Environment
+variables '$DIARY_DIR', '$EDITOR' and '$LANG' for locale ('weekday')
+.TP
+4.
+.B
+Option
+arguments, see section
+.B
+OPTIONS
+.TP
+5.
+First non-option argument \fIDIRECTORY\fR is interpreted as 'DIARY_DIR'
+
+.SH PRECEDENCE EXAMPLE: LOCALE AND FIRST DAY OF WEEK
+If glibc is installed, the first weekday defaults to the locale defined in the current shell
+environment ($LANG, see man locale), unless specified otherwise via the '--weekday'/'-w'.
+
+.nf
+# start with weekday=3(Wed), overrule any other configuration value
+diary -w3
+
+# start with glibc derived weekday=1, regardless of 'weekday' in config file
+LANG=de_CH diary
+
+# if glibc is installed, start with glibc derived base date (weekday=0)
+LANG= diary
+
+# disable environment variable, default to value from config file
+unset LANG
+
+# start with 'weekday' default from config file, if available
+diary
+
+# remove config file
+rm ${XDG_CONFIG_HOME:-~/.config}/diary/diary.cfg
+
+# start with 'weekday' default value from source code (1=Mon)
+diary
+.fi
+
+.SH DEVELOPMENT
+All source code is available in this github repository:
+<https://github.com/in0rdr/diary>. Contributions are always welcome!
diff --git a/man/diary.1.html b/man/diary.1.html
@@ -0,0 +1,319 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8"/>
+ <style>
+ table.head, table.foot { width: 100%; }
+ td.head-rtitle, td.foot-os { text-align: right; }
+ td.head-vol { text-align: center; }
+ div.Pp { margin: 1ex 0ex; }
+ div.Nd, div.Bf, div.Op { display: inline; }
+ span.Pa, span.Ad { font-style: italic; }
+ span.Ms { font-weight: bold; }
+ dl.Bl-diag > dt { font-weight: bold; }
+ code.Nm, code.Fl, code.Cm, code.Ic, code.In, code.Fd, code.Fn,
+ code.Cd { font-weight: bold; font-family: inherit; }
+ </style>
+ <title>DIARY(1)</title>
+</head>
+<body>
+<table class="head">
+ <tr>
+ <td class="head-ltitle">DIARY(1)</td>
+ <td class="head-vol">General Commands Manual</td>
+ <td class="head-rtitle">DIARY(1)</td>
+ </tr>
+</table>
+<div class="manual-text">
+<h1 class="Sh" title="Sh" id="NAME"><a class="permalink" href="#NAME">NAME</a></h1>
+diary - Text-based journaling program
+<div class="Pp"></div>
+<h1 class="Sh" title="Sh" id="SYNOPSIS"><a class="permalink" href="#SYNOPSIS">SYNOPSIS</a></h1>
+<b>diary</b> [ <i>OPTION</i>]... [<i>DIRECTORY</i>]...
+<div> </div>
+<div class="Pp"></div>
+<h1 class="Sh" title="Sh" id="DESCRIPTION"><a class="permalink" href="#DESCRIPTION">DESCRIPTION</a></h1>
+<b>diary</b> is a text-based program for managing journal entries.
+<div class="Pp"></div>
+<h1 class="Sh" title="Sh" id="OPTIONS"><a class="permalink" href="#OPTIONS">OPTIONS</a></h1>
+<dl class="Bl-tag">
+ <dt><b>-v</b>, <b>--version</b></dt>
+ <dd>Print diary version</dd>
+</dl>
+<dl class="Bl-tag">
+ <dt><b>-h</b>, <b>--help</b></dt>
+ <dd>Show diary help text</dd>
+</dl>
+<dl class="Bl-tag">
+ <dt><b>-d</b>, <b>--dir</b>=<i>DIARY_DIR</i></dt>
+ <dd>Diary storage directory DIARY_DIR</dd>
+</dl>
+<dl class="Bl-tag">
+ <dt><b>-e</b>, <b>--editor</b>=<i>EDITOR</i></dt>
+ <dd>EDITOR is the text editor used for opening the journal files.</dd>
+</dl>
+<dl class="Bl-tag">
+ <dt><b>-f</b>, <b>--fmt</b>=<i>FMT</i></dt>
+ <dd>FMT is a custom date and file format. Change with care, because the diary
+ reads and writes to files with file name FMT. The new FMT is only applied
+ to newly saved entries. Existing entries with the old FMT are not
+ automatically migrated to the new FMT and do not show up with a new FMT
+ specifier. Consequently, a change in FMT shows an empty diary at first.
+ Rename all files in the DIARY_DIR to migrate to a new FMT.</dd>
+</dl>
+<dl class="Bl-tag">
+ <dt><b>-r</b>, <b>--range</b>=<i>RANGE</i></dt>
+ <dd>RANGE is the number of years to show before/after todays date. Defaults to
+ 1 year.</dd>
+</dl>
+<dl class="Bl-tag">
+ <dt><b>-f</b>, <b>--weekday</b>=<i>DAY</i></dt>
+ <dd>First day of the week. DAY is an integer in range (0..6), interpreted as 0
+ or 7 = Sun, 1 = Mon, ..., 6 = Sat. If glibc is installed, the first day of
+ the week is derived from the current locale setting ('$LANG', see man
+ locale). Without glibc, the first weekday defaults to 1 (Monday), unless
+ specified otherwise with this option.
+ <div class="Pp"></div>
+ </dd>
+</dl>
+<h1 class="Sh" title="Sh" id="NAVIGATION"><a class="permalink" href="#NAVIGATION">NAVIGATION</a></h1>
+Navigation is done using the following vim-inspired keyboard shortcuts:
+<div class="Pp"></div>
+<table class="tbl">
+ <tr>
+ <td>Key(s) </td>
+ <td> Action</td>
+ </tr>
+ <tr>
+ <td>====== </td>
+ <td> ======</td>
+ </tr>
+ <tr>
+ <td>e, Enter </td>
+ <td> edit current entry</td>
+ </tr>
+ <tr>
+ <td>d, x </td>
+ <td> delete current entry</td>
+ </tr>
+ <tr>
+ <td>s </td>
+ <td> sync current entry with CalDAV server</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>t </td>
+ <td> jump to today</td>
+ </tr>
+ <tr>
+ <td>f </td>
+ <td> jump to or find specific day</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>j, down </td>
+ <td> go forward by 1 week</td>
+ </tr>
+ <tr>
+ <td>k, up </td>
+ <td> go backward by 1 week</td>
+ </tr>
+ <tr>
+ <td>h, left </td>
+ <td> go left by 1 day</td>
+ </tr>
+ <tr>
+ <td>l, right </td>
+ <td> go right by 1 day</td>
+ </tr>
+ <tr>
+ <td>J </td>
+ <td> go forward by 1 month</td>
+ </tr>
+ <tr>
+ <td>K </td>
+ <td> go backward by 1 month</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>N </td>
+ <td> go to the previous journal entry</td>
+ </tr>
+ <tr>
+ <td>n </td>
+ <td> go to the next journal entry</td>
+ </tr>
+ <tr>
+ <td>g </td>
+ <td> go to the first journal entry</td>
+ </tr>
+ <tr>
+ <td>G </td>
+ <td> go to the last journal entry</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>q </td>
+ <td> quit the program</td>
+ </tr>
+</table>
+<div class="Pp"></div>
+<h1 class="Sh" title="Sh" id="ENVIRONMENT"><a class="permalink" href="#ENVIRONMENT">ENVIRONMENT</a></h1>
+<dl class="Bl-tag">
+ <dt>DIARY_DIR</dt>
+ <dd>If this variable is set to a directory that can be opened, <b>diary</b>
+ will use it to store diary files. Diary files are simple text files named
+ after their date, formatted according to FMT (see '-f'/'--fmt' options and
+ (see man strftime). All other files different from FMT are ignored.
+ <div class="Pp"></div>
+ </dd>
+</dl>
+<dl class="Bl-tag">
+ <dt>EDITOR</dt>
+ <dd>The program used to edit journal entries.
+ <div class="Pp"></div>
+ </dd>
+</dl>
+<dl class="Bl-tag">
+ <dt>LANG</dt>
+ <dd>The default locale used to display the first day of the week.
+ <div class="Pp"></div>
+ </dd>
+</dl>
+<h1 class="Sh" title="Sh" id="ARGUMENTS"><a class="permalink" href="#ARGUMENTS">ARGUMENTS</a></h1>
+If the argument <i>DIRECTORY</i> is given, diary files are read from and stored
+ to that directory, ignoring the DIARY_DIR environment variable, any
+ '-d'/'--dir' options or the 'dir' value set in the config file.
+<div class="Pp"></div>
+<h1 class="Sh" title="Sh" id="FILES"><a class="permalink" href="#FILES">FILES</a></h1>
+<dl class="Bl-tag">
+ <dt><i>${PREFIX:-/usr/local/bin}/diary</i></dt>
+ <dd>The diary binary</dd>
+</dl>
+<dl class="Bl-tag">
+ <dt><i>${XDG_CONFIG_HOME:-~/.config}/diary/diary.cfg</i></dt>
+ <dd>An optional diary configuration file
+ <div class="Pp"></div>
+ </dd>
+</dl>
+<h1 class="Sh" title="Sh" id="CONFIGURATION_FILE"><a class="permalink" href="#CONFIGURATION_FILE">CONFIGURATION
+ FILE</a></h1>
+The diary.cfg configuration file can optionally be used to persist diary
+ configuration. Use the '#' or ';' characters to comment lines.
+<div class="Pp"></div>
+Create default config location:
+<div class="Pp"></div>
+<pre>
+ mkdir -p ${XDG_CONFIG_HOME:-~/.config}/diary
+</pre>
+<div class="Pp"></div>
+Install an example config file with defaults:
+<div class="Pp"></div>
+<br/>
+<pre>
+tee ${XDG_CONFIG_HOME:-~/.config}/diary/diary.cfg <<EOF
+# Path that holds the journal text files
+#dir = ~/diary
+# Number of years to show before/after todays date
+range = 1
+# 0 = Sunday, 1 = Monday, ..., 6 = Saturday
+weekday = 1
+# Date and file format, change with care
+fmt = %Y-%m-%d
+# Editor to open journal files with
+editor =
+# Google calendar name for CalDAV sync
+#google_calendar =
+# Google OAuth2 clientid and secretid
+#google_clientid =
+#google_secretid =
+# Google OAuth2 tokenfile
+#google_tokenfile = ~/.diary-token
+EOF
+</pre>
+<div class="Pp"></div>
+<h1 class="Sh" title="Sh" id="PRECEDENCE_RULES"><a class="permalink" href="#PRECEDENCE_RULES">PRECEDENCE
+ RULES</a></h1>
+The default variables, for instance, for the configuration variables 'editor',
+ 'dir' and 'weekday', are populated with values in the following order:
+<div class="Pp"></div>
+<dl class="Bl-tag">
+ <dt>1.</dt>
+ <dd>No default for 'DIARY_DIR'. Defaults for 'range', 'weekday', 'fmt' and
+ 'editor' are provided in 'diary.h'. If 'EDITOR' is unset and no editor is
+ provided in the config file or via the '-e' option, the <b>diary</b> works
+ read-only. Journal files cannot be opened. If 'DIARY_DIR' is not provided,
+ the <b>diary</b> won't open.</dd>
+</dl>
+<dl class="Bl-tag">
+ <dt>2.</dt>
+ <dd><b>Config file</b> (empty default for 'editor', no default for 'dir')</dd>
+</dl>
+<dl class="Bl-tag">
+ <dt>3.</dt>
+ <dd><b>Environment</b> variables '$DIARY_DIR', '$EDITOR' and '$LANG' for
+ locale ('weekday')</dd>
+</dl>
+<dl class="Bl-tag">
+ <dt>4.</dt>
+ <dd><b>Option</b> arguments, see section <b>OPTIONS</b></dd>
+</dl>
+<dl class="Bl-tag">
+ <dt>5.</dt>
+ <dd>First non-option argument <i>DIRECTORY</i> is interpreted as 'DIARY_DIR'
+ <div class="Pp"></div>
+ </dd>
+</dl>
+<h1 class="Sh" title="Sh" id="PRECEDENCE_EXAMPLE:_LOCALE_AND_FIRST_DAY_OF_WEEK"><a class="permalink" href="#PRECEDENCE_EXAMPLE:_LOCALE_AND_FIRST_DAY_OF_WEEK">PRECEDENCE
+ EXAMPLE: LOCALE AND FIRST DAY OF WEEK</a></h1>
+If glibc is installed, the first weekday defaults to the locale defined in the
+ current shell environment ($LANG, see man locale), unless specified otherwise
+ via the '--weekday'/'-w'.
+<div class="Pp"></div>
+<pre>
+# start with weekday=3(Wed), overrule any other configuration value
+diary -w3
+<div class="Pp"></div>
+# start with glibc derived weekday=1, regardless of 'weekday' in config file
+LANG=de_CH diary
+<div class="Pp"></div>
+# if glibc is installed, start with glibc derived base date (weekday=0)
+LANG= diary
+<div class="Pp"></div>
+# disable environment variable, default to value from config file
+unset LANG
+<div class="Pp"></div>
+# start with 'weekday' default from config file, if available
+diary
+<div class="Pp"></div>
+# remove config file
+rm ${XDG_CONFIG_HOME:-~/.config}/diary/diary.cfg
+<div class="Pp"></div>
+# start with 'weekday' default value from source code (1=Mon)
+diary
+</pre>
+<div class="Pp"></div>
+<h1 class="Sh" title="Sh" id="DEVELOPMENT"><a class="permalink" href="#DEVELOPMENT">DEVELOPMENT</a></h1>
+All source code is available in this github repository:
+ <https://github.com/in0rdr/diary>. Contributions are always
+ welcome!</div>
+<table class="foot">
+ <tr>
+ <td class="foot-date"></td>
+ <td class="foot-os"></td>
+ </tr>
+</table>
+</body>
+</html>
diff --git a/src/caldav.c b/src/caldav.c
@@ -0,0 +1,877 @@
+#include "caldav.h"
+
+CURL *curl;
+char* access_token;
+char* refresh_token;
+int token_ttl;
+
+// Local bind address for receiving OAuth callbacks.
+// Reserve 2 chars for the ipv6 square brackets.
+char ip[INET6_ADDRSTRLEN], ipstr[INET6_ADDRSTRLEN+2];
+
+/* Write a random code challenge of size len to dest */
+void random_code_challenge(size_t len, char* dest) {
+ // https://developers.google.com/identity/protocols/oauth2/native-app#create-code-challenge
+ // A code_verifier is a random string using characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
+ srand(time(NULL));
+
+ char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
+ size_t alphabet_size = strlen(alphabet);
+
+ for (int i = 0; i < len; i++) {
+ dest[i] = alphabet[rand() % alphabet_size];
+ }
+ dest[len-1] = '\0';
+}
+
+static size_t curl_write_mem_callback(void * contents, size_t size, size_t nmemb, void *userp) {
+ size_t realsize = size * nmemb;
+ struct curl_mem_chunk* mem = (struct curl_mem_chunk*)userp;
+
+ char* ptr = realloc(mem->memory, mem->size + realsize + 1);
+ if (!ptr) {
+ fprintf(stderr, "not enough memory (realloc in CURLOPT_WRITEFUNCTION returned NULL)\n");
+ return 0;
+ }
+
+ mem->memory = ptr;
+ memcpy(&(mem->memory[mem->size]), contents, realsize);
+ mem->size += realsize;
+ mem->memory[mem->size] = 0;
+
+ return realsize;
+}
+
+static size_t write_data(void *ptr, size_t size, size_t nmemb, void *stream) {
+ size_t written = fwrite(ptr, size, nmemb, (FILE *)stream);
+ return written;
+}
+
+// todo
+// https://beej.us/guide/bgnet/html
+void* get_in_addr(struct sockaddr *sa) {
+ if (sa->sa_family == AF_INET) {
+ return &(((struct sockaddr_in*)sa)->sin_addr);
+ }
+
+ return &(((struct sockaddr_in6*)sa)->sin6_addr);
+}
+
+/*
+* Extract OAuth2 code from http header. Reads the code from
+* the http header in http_header and writes the OAuth2 code
+* to the char string pointed to by code.
+*/
+char* extract_oauth_code(char* http_header) {
+ // example token: "/?code=&scope="
+ char* res = strtok(http_header, " ");
+ while (res != NULL) {
+ if (strstr(res, "code") != NULL) {
+ res = strtok(res, "="); // code key
+ res = strtok(NULL, "&"); // code value
+ fprintf(stderr, "Code: %s\n", res);
+ break;
+ }
+ res = strtok(NULL, " ");
+ }
+ return res;
+}
+
+char* read_tokenfile() {
+ FILE* token_file;
+ char* token_buf;
+ long token_bytes;
+
+ char* tokenfile_path = expand_path(CONFIG.google_tokenfile);
+ token_file = fopen(tokenfile_path, "r");
+ free(tokenfile_path);
+
+ if (token_file == NULL) {
+ perror("Failed to open tokenfile");
+ return NULL;
+ }
+
+ fseek(token_file, 0, SEEK_END);
+ token_bytes = ftell(token_file) + 1;
+ rewind(token_file);
+
+ token_buf = malloc(token_bytes);
+ if (token_buf != NULL) {
+ fread(token_buf, sizeof(char), token_bytes, token_file);
+ token_buf[token_bytes] = '\0';
+
+ access_token = extract_json_value(token_buf, "access_token", true);
+
+ // program segfaults if NULL value is provided to atoi
+ char* token_ttl_str = extract_json_value(token_buf, "expires_in", false);
+ if (token_ttl_str == NULL) {
+ token_ttl = 0;
+ } else {
+ token_ttl = atoi(token_ttl_str);
+ }
+
+ // only update the existing refresh token if the request actually
+ // contained a valid refresh_token, i.e, if it was the initial
+ // interactive authZ request from token code confirmed by the user
+ char* new_refresh_token = extract_json_value(token_buf, "refresh_token", true);
+ if (new_refresh_token != NULL) {
+ refresh_token = new_refresh_token;
+ }
+
+ fprintf(stderr, "Access token: %s\n", access_token);
+ fprintf(stderr, "Token TTL: %i\n", token_ttl);
+ fprintf(stderr, "Refresh token: %s\n", refresh_token);
+ } else {
+ perror("malloc failed");
+ return NULL;
+ }
+ fclose(token_file);
+ return token_buf;
+}
+
+void write_tokenfile() {
+ char* tokenfile_path = expand_path(CONFIG.google_tokenfile);
+ FILE* tokenfile = fopen(tokenfile_path, "wb");
+ free(tokenfile_path);
+
+ if (tokenfile == NULL) {
+ perror("Failed to open tokenfile");
+ } else {
+ char contents[1000];
+ char* tokenfile_contents = "{\n"
+ " \"access_token\": \"%s\",\n"
+ " \"expires_in\": %i,\n"
+ " \"refresh_token\": \"%s\"\n"
+ "}\n";
+ sprintf(contents, tokenfile_contents,
+ access_token,
+ token_ttl,
+ refresh_token);
+ fputs(contents, tokenfile);
+ }
+ fclose(tokenfile);
+
+ chmod(tokenfile_path, S_IRUSR|S_IWUSR);
+ perror("chmod");
+
+
+ char* tokfile = read_tokenfile();
+ fprintf(stderr, "New tokenfile contents: %s\n", tokfile);
+ fprintf(stderr, "New Access token: %s\n", access_token);
+ fprintf(stderr, "New Token TTL: %i\n", token_ttl);
+ fprintf(stderr, "Refresh token: %s\n", refresh_token);
+ free(tokfile);
+}
+
+void get_access_token(char* code, char* verifier, bool refresh) {
+ CURLcode res;
+ char* tokfile;
+
+ char postfields[500];
+ if (refresh) {
+ sprintf(postfields, "client_id=%s&client_secret=%s&grant_type=refresh_token&refresh_token=%s",
+ CONFIG.google_clientid,
+ CONFIG.google_secretid,
+ refresh_token);
+ } else {
+ sprintf(postfields, "client_id=%s&client_secret=%s&code=%s&code_verifier=%s&grant_type=authorization_code&redirect_uri=http://%s:%i",
+ CONFIG.google_clientid,
+ CONFIG.google_secretid,
+ code,
+ verifier,
+ ipstr,
+ GOOGLE_OAUTH_REDIRECT_PORT);
+ }
+ fprintf(stderr, "CURLOPT_POSTFIELDS: %s\n", postfields);
+
+ curl = curl_easy_init();
+
+ FILE* tokenfile;
+ char* tokenfile_path;
+
+ if (curl) {
+ curl_easy_setopt(curl, CURLOPT_URL, GOOGLE_OAUTH_TOKEN_URL);
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
+ //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
+
+ tokenfile_path = expand_path(CONFIG.google_tokenfile);
+ tokenfile = fopen(tokenfile_path, "wb");
+ free(tokenfile_path);
+
+ if (tokenfile == NULL) {
+ perror("Failed to open tokenfile");
+ } else {
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, tokenfile);
+ res = curl_easy_perform(curl);
+ fclose(tokenfile);
+ }
+
+ curl_easy_cleanup(curl);
+
+ if (res != CURLE_OK) {
+ fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
+ return;
+ }
+ // update global variables from tokenfile
+ // this will also init the access_token var the first time
+ tokfile = read_tokenfile();
+ free(tokfile);
+ // Make sure the refresh token is re-written and persistet
+ // to the tokenfile for further requests, because the
+ // is not returned by the refresh_token call:
+ // https://developers.google.com/identity/protocols/oauth2/native-app#offline
+ write_tokenfile();
+ }
+}
+
+char* get_oauth_code(const char* verifier, WINDOW* header) {
+ struct addrinfo hints, *addr_res;
+
+ memset(&hints, 0, sizeof(struct addrinfo));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+
+ int status;
+ if ((status=getaddrinfo(NULL, MKSTR(GOOGLE_OAUTH_REDIRECT_PORT), &hints, &addr_res)) != 0) {
+ fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
+ }
+
+ void *addr;
+ char *ipver;
+ //todo: extract
+ //addr = get_in_addr(addr_res->ai_addr);
+ if (addr_res->ai_family == AF_INET) {
+ struct sockaddr_in *ipv4 = (struct sockaddr_in *) addr_res->ai_addr;
+ addr = &(ipv4->sin_addr);
+ ipver = "IPv4";
+ } else { // IPv6
+ struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *) addr_res->ai_addr;
+ addr = &(ipv6->sin6_addr);
+ ipver = "IPv6";
+ }
+
+ inet_ntop(addr_res->ai_family, addr, ip, sizeof ip);
+ if (strcmp("IPv6", ipver) == 0) {
+ sprintf(ipstr, "[%s]", ip);
+ }
+
+ // Show Google OAuth URI
+ char uri[500];
+ sprintf(uri, "%s?scope=%s&code_challenge=%s&response_type=%s&redirect_uri=http://%s:%i&client_id=%s",
+ GOOGLE_OAUTH_AUTHZ_URL,
+ GOOGLE_OAUTH_SCOPE,
+ verifier,
+ GOOGLE_OAUTH_RESPONSE_TYPE,
+ ipstr,
+ GOOGLE_OAUTH_REDIRECT_PORT,
+ CONFIG.google_clientid);
+ fprintf(stderr, "Google OAuth2 authorization URI: %s\n", uri);
+
+ // Show the Google OAuth2 authorization URI in the header
+ wclear(header);
+ wresize(header, LINES, getmaxx(header));
+ mvwprintw(header, 0, 0, "Go to Google OAuth2 authorization URI. Use 'q' or 'Ctrl+c' to quit authorization process.\n%s", uri);
+ wrefresh(header);
+
+ int socketfd = socket(addr_res->ai_family, addr_res->ai_socktype, addr_res->ai_protocol);
+ if (socketfd < 0) {
+ perror("Error opening socket");
+ }
+
+ // reuse socket address
+ int yes=1;
+ setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
+
+ if (bind(socketfd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
+ perror("Error binding socket");
+ }
+
+ freeaddrinfo(addr_res);
+
+ int ls = listen(socketfd, GOOGLE_OAUTH_REDIRECT_SOCKET_BACKLOG);
+ if (ls < 0) {
+ perror("Listen error");
+ }
+
+ struct pollfd pfds[2];
+
+ pfds[0].fd = STDIN_FILENO;
+ pfds[0].events = POLLIN;
+ pfds[1].fd = socketfd;
+ pfds[1].events = POLLIN;
+ int fd_count = 2;
+
+ int connfd, bytes_rec, bytes_sent;
+ char http_header[8*1024];
+ char* reply =
+ "HTTP/1.1 200 OK\n"
+ "Content-Type: text/html\n"
+ "Connection: close\n\n"
+ "<html>"
+ "<head><title>Authorization successfull</title></head>"
+ "<body>"
+ "<p><b>Authorization successfull.</b></p>"
+ "<p>You consented that diary can access your Google calendar.<br/>"
+ "Pleasee close this window and return to diary.</p>"
+ "</body>"
+ "</html>";
+
+ // Handle descriptors read-to-read (POLLIN),
+ // stdin or server socker, whichever is first
+ for (;;) {
+ int poll_count = poll(pfds, fd_count, -1);
+
+ if (poll_count == -1) {
+ perror("Erro in poll");
+ break;
+ }
+
+ // Cancel through stdin
+ if (pfds[0].revents & POLLIN) {
+ noecho();
+ int ch = wgetch(header);
+ echo();
+ // sudo showkey -a
+ // Ctrl+c: ^C 0x03
+ // q : q 0x71
+ if (ch == 0x03 || ch == 0x71) {
+ fprintf(stderr, "Escape char: %x\n", ch);
+ fprintf(stderr, "Hanging up, closing server socket\n");
+ break;
+ }
+ }
+ if (pfds[1].revents & POLLIN) {
+ // accept connections but ignore client addr
+ connfd = accept(socketfd, NULL, NULL);
+ if (connfd < 0) {
+ perror("Error accepting connection");
+ break;
+ }
+
+ bytes_rec = recv(connfd, http_header, sizeof http_header, 0);
+ if (bytes_rec < 0) {
+ perror("Error reading stream message");
+ break;
+ }
+ fprintf(stderr, "Received http header: %s\n", http_header);
+
+ bytes_sent = send(connfd, reply, strlen(reply), 0);
+ if (bytes_sent < 0) {
+ perror("Error sending");
+ }
+ fprintf(stderr, "Bytes sent: %i\n", bytes_sent);
+
+ close(connfd);
+ break;
+ }
+ } // end for ;;
+
+ // close server socket
+ close(pfds[1].fd);
+
+ char* code = extract_oauth_code(http_header);
+ if (code == NULL) {
+ fprintf(stderr, "Found no OAuth code in http header.\n");
+ return NULL;
+ }
+ fprintf(stderr, "OAuth code: %s\n", code);
+
+ return code;
+}
+
+char* caldav_req(struct tm* date, char* url, char* http_method, char* postfields, int depth) {
+ // only support depths 0 or 1
+ if (depth < 0 || depth > 1) {
+ return NULL;
+ }
+
+ // if access_token is NULL, the program segfaults
+ // while construcing the bearer_token below
+ if (access_token == NULL) {
+ return NULL;
+ }
+
+ CURLcode res;
+
+ curl = curl_easy_init();
+
+ // https://curl.se/libcurl/c/getinmemory.html
+ struct curl_mem_chunk caldav_resp;
+ caldav_resp.memory = malloc(1);
+ if (caldav_resp.memory == NULL) {
+ perror("malloc failed");
+ return NULL;
+ }
+ caldav_resp.size = 0;
+
+ if (curl) {
+ // fail if not authenticated, !CURLE_OK
+ curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
+ // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method);
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_mem_callback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&caldav_resp);
+
+ // construct header fields
+ struct curl_slist *header = NULL;
+ char bearer_token[strlen("Authorization: Bearer")+strlen(access_token)];
+ sprintf(bearer_token, "Authorization: Bearer %s", access_token);
+ char depth_header[strlen("Depth: 0")];
+ sprintf(depth_header, "Depth: %i", depth);
+ header = curl_slist_append(header, depth_header);
+ header = curl_slist_append(header, bearer_token);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header);
+
+ // set postfields, if any
+ if (postfields != NULL) {
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields);
+ }
+
+ res = curl_easy_perform(curl);
+
+ curl_easy_cleanup(curl);
+
+ fprintf(stderr, "Curl retrieved %lu bytes\n", (unsigned long)caldav_resp.size);
+ fprintf(stderr, "Curl content: %s\n", caldav_resp.memory);
+
+ if (res != CURLE_OK) {
+ fprintf(stderr, "Curl response: %s\n", caldav_resp.memory);
+ fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
+ return NULL;
+ }
+ }
+
+ return caldav_resp.memory;
+}
+
+// return current user principal from CalDAV XML response
+char* parse_caldav_current_user_principal(char* xml) {
+ char* xml_key_pos = strstr(xml, "<D:current-user-principal>");
+ // this XML does not contain a user principal at all
+ if (xml_key_pos == NULL) {
+ return NULL;
+ }
+
+ fprintf(stderr, "Found current-user-principal at position: %i\n", *xml_key_pos);
+
+ //<D:current-user-principal>
+ //<D:href>/caldav/v2/diary.in0rdr%40gmail.com/user</D:href>
+ char* tok = strtok(xml_key_pos, "<"); // D:current-user-principal>
+ if (tok != NULL) {
+ tok = strtok(NULL, "<"); // D:href>/caldav/v2/test%40gmail.com/user
+ fprintf(stderr, "First token: %s\n", tok);
+ tok = strstr(tok, ">"); // >/caldav/v2/test%40gmail.com/user
+ tok++; // cut >
+ char* tok_end = strrchr(tok, '/');
+ *tok_end = '\0'; // cut /user
+ }
+
+ return tok;
+}
+
+// return calendar uri from CalDAV home set XML response
+char* parse_caldav_calendar(char* xml, char* calendar) {
+ char displayname_needle[strlen(calendar) + strlen("<D:displayname></D:displayname>")];
+ sprintf(displayname_needle, "<D:displayname>%s</D:displayname>", calendar);
+ fprintf(stderr, "Displayname needle: %s\n", displayname_needle);
+ char* displayname_pos = strstr(xml, displayname_needle);
+ // this XML multistatus response does not contain the users calendar
+ if (displayname_pos == NULL) {
+ return NULL;
+ }
+
+ // <D:response>
+ // <D:href>/caldav/v2/2fcv7j5mf38o5u2kg5tar4baao%40group.calendar.google.com/events/</D:href>
+ // <D:propstat>
+ // <D:status></D:status>
+ // <D:prop>
+ // <D:displayname>diary</D:displayname>
+ // </D:prop>
+ // </D:propstat>
+ // </D:response>
+
+ // shorten multistatus response and find last hyperlink
+ *displayname_pos= '\0';
+ char* href = strrstr(xml, "<D:href>");
+ if (href != NULL) {
+ fprintf(stderr, "Found calendar href: %s\n", href);
+ href = strtok(href, "<"); // :href>/caldav/v2/aaa%40group.calendar.google.com/events/
+ if (href != NULL) {
+ href = strchr(href, '>');
+ href++; // cut >
+ fprintf(stderr, "Found calendar href: %s\n", href);
+ }
+ return href;
+ }
+ return NULL;
+}
+
+void put_event(struct tm* date, const char* dir, size_t dir_size, char* calendar_uri) {
+ // get entry path
+ char path[100];
+ char* ppath = path;
+ char* descr;
+ long descr_bytes;
+
+ fpath(dir, dir_size, date, &ppath, sizeof path);
+ if (ppath == NULL) {
+ fprintf(stderr, "Error while retrieving file path for diary reading");
+ return;
+ }
+
+ FILE* fp = fopen(path, "r");
+ if (fp == NULL) perror("Error opening file");
+
+ fseek(fp, 0, SEEK_END);
+ descr_bytes = ftell(fp);
+ rewind(fp);
+
+ size_t descr_labell = strlen("DESCRIPTION:");
+ size_t descrl = descr_bytes + descr_labell + 1;
+ descr = malloc(descrl);
+ if (descr == NULL) {
+ perror("malloc failed");
+ return;
+ }
+
+ descr[0] = '\0';
+ strcat(descr, "DESCRIPTION:");
+
+ int items_read = fread(descr + descr_labell, sizeof(char), descr_bytes, fp);
+ if (items_read != descr_bytes) {
+ fprintf(stderr, "Read %i items but expected %li, aborting.", items_read, descr_bytes);
+ return;
+ }
+
+ descr[descrl] = '\0';
+ fprintf(stderr, "File buffer that will be uploaded to the remote CalDAV server:\n%s\n", descr);
+
+ char* folded_descr = fold(descr);
+ fprintf(stderr, "Folded descr:%s\n", folded_descr);
+
+ char uid[9];
+ strftime(uid, sizeof uid, "%Y%m%d", date);
+
+ char* ics = "BEGIN:VCALENDAR\n"
+ "BEGIN:VEVENT\n"
+ "UID:%s\n"
+ "DTSTART;VALUE=DATE:%s\n"
+ "SUMMARY:%s\n"
+ "%s\n"
+ "END:VEVENT\n"
+ "END:VCALENDAR";
+ char postfields[strlen(ics) + strlen(folded_descr) + 100];
+ sprintf(postfields, ics,
+ uid,
+ uid,
+ uid, // todo: display first few chars of DESCRIPTION as SUMMARY
+ folded_descr);
+
+ fprintf(stderr, "PUT event postfields:\n%s\n", postfields);
+
+ strcat(calendar_uri, uid);
+ strcat(calendar_uri, ".ics");
+ fprintf(stderr, "Event uri:\n%s\n", calendar_uri);
+ char* response = caldav_req(date, calendar_uri, "PUT", postfields, 0);
+ fprintf(stderr, "PUT event response:\n%s\n", response);
+ fclose(fp);
+ free(folded_descr);
+ free(descr);
+
+ if (response == NULL) {
+ fprintf(stderr, "PUT event failed.\n");
+ }
+}
+
+void* show_progress(void* vargp){
+ WINDOW* header = (WINDOW*) vargp;
+ mvwprintw(header, 0, COLS - CAL_WIDTH - ASIDE_WIDTH - 11, " syncing ");
+ for(;;) {
+ mvwprintw(header, 0, COLS - CAL_WIDTH - ASIDE_WIDTH - 10, "|");
+ wrefresh(header);
+ usleep(200000);
+ mvwprintw(header, 0, COLS - CAL_WIDTH - ASIDE_WIDTH - 10, "/");
+ wrefresh(header);
+ usleep(200000);
+ mvwprintw(header, 0, COLS - CAL_WIDTH - ASIDE_WIDTH - 10, "-");
+ wrefresh(header);
+ usleep(200000);
+ mvwprintw(header, 0, COLS - CAL_WIDTH - ASIDE_WIDTH - 10, "\\");
+ wrefresh(header);
+ usleep(200000);
+ }
+}
+
+/*
+* Sync with CalDAV server.
+* Returns the answer char of the confirmation dialogue
+* Returns 0 if neither local nor remote file exists.
+* Otherwise, returns -1 on error.
+*/
+int caldav_sync(struct tm* date,
+ WINDOW* header,
+ WINDOW* cal,
+ int pad_pos,
+ const char* dir,
+ size_t dir_size,
+ bool confirm) {
+ pthread_t progress_tid;
+ pthread_create(&progress_tid, NULL, show_progress, (void*)header);
+
+ // fetch existing API tokens
+ char* tokfile = read_tokenfile();
+ free(tokfile);
+
+ if (access_token == NULL && refresh_token == NULL) {
+ // no access token exists yet, create new verifier
+ char challenge[GOOGLE_OAUTH_CODE_VERIFIER_LENGTH];
+ random_code_challenge(GOOGLE_OAUTH_CODE_VERIFIER_LENGTH, challenge);
+ fprintf(stderr, "Challenge/Verifier: %s\n", challenge);
+
+ // fetch new code with verifier
+ char* code = get_oauth_code(challenge, header);
+ if (code == NULL) {
+ fprintf(stderr, "Error retrieving access code.\n");
+ return -1;
+ }
+
+ // get acess token using code and verifier
+ get_access_token(code, challenge, false);
+ }
+
+ char* principal_postfields = "<d:propfind xmlns:d='DAV:' xmlns:cs='http://calendarserver.org/ns/'>"
+ "<d:prop><d:current-user-principal/></d:prop>"
+ "</d:propfind>";
+
+
+ // check if we can use the token from the tokenfile
+ char* user_principal = caldav_req(date, GOOGLE_CALDAV_URI, "PROPFIND", principal_postfields, 0);
+ fprintf(stderr, "User principal: %s\n", user_principal);
+
+ if (user_principal == NULL) {
+ fprintf(stderr, "Unable to fetch principal, refreshing API token.\n");
+ // The principal could not be fetched,
+ // get new acess token with refresh token
+ get_access_token(NULL, NULL, true);
+ // Retry request for event with new token
+ user_principal = caldav_req(date, GOOGLE_CALDAV_URI, "PROPFIND", principal_postfields, 0);
+ }
+
+ char* tokenfile_path = expand_path(CONFIG.google_tokenfile);
+ if (user_principal == NULL) {
+ fprintf(stderr, "Unable to fetch principal due to invalid tokenfile. Removing tokenfile '%s'.\n", CONFIG.google_tokenfile);
+
+ wclear(header);
+ mvwprintw(header, 0, 0, "Invalid Google OAuth2 credentials, removing tokenfile at '%s'. Please retry.", CONFIG.google_tokenfile);
+ wrefresh(header);
+
+ // accept any input to proceed
+ noecho();
+ wgetch(header);
+ echo();
+
+ if (unlink(tokenfile_path) == -1) {
+ perror("unlink tokenfile");
+ }
+
+ free(access_token);
+ access_token = NULL;
+ free(refresh_token);
+ refresh_token = NULL;
+ pthread_cancel(progress_tid);
+ wclear(header);
+ return -1;
+ }
+
+ user_principal = parse_caldav_current_user_principal(user_principal);
+ fprintf(stderr, "\nUser Principal: %s\n", user_principal);
+
+ // get the home-set of the user
+ char uri[300];
+ sprintf(uri, "%s%s", GOOGLE_API_URI, user_principal);
+ fprintf(stderr, "\nHome Set URI: %s\n", uri);
+ char* home_set = caldav_req(date, uri, "PROPFIND", "", 1);
+ fprintf(stderr, "\nHome Set: %s\n", home_set);
+
+ // get calendar URI from the home-set
+ char* calendar_href = parse_caldav_calendar(home_set, CONFIG.google_calendar);
+ fprintf(stderr, "\nCalendar href: %s\n", calendar_href);
+
+ char* xml_filter = "<c:calendar-query xmlns:d='DAV:' xmlns:c='urn:ietf:params:xml:ns:caldav'>"
+ "<d:prop><c:calendar-data/></d:prop>"
+ "<c:filter><c:comp-filter name='VCALENDAR'>"
+ "<c:comp-filter name='VEVENT'>"
+ "<c:time-range start='%s' end='%s'/></c:comp-filter>"
+ "</c:comp-filter></c:filter></c:calendar-query>";
+
+ // construct next day from date+1
+ time_t date_time = mktime(date);
+ struct tm* next_day = localtime(&date_time);
+ next_day->tm_mday++;
+ mktime(next_day);
+
+ char dstr_cursor[30];
+ char dstr_next_day[30];
+
+ char* format = "%Y%m%dT000000Z";
+ strftime(dstr_cursor, sizeof dstr_cursor, format, date);
+ strftime(dstr_next_day, sizeof dstr_next_day, format, next_day);
+
+ char caldata_postfields[strlen(xml_filter)+50];
+ sprintf(caldata_postfields, xml_filter,
+ dstr_cursor,
+ dstr_next_day);
+ fprintf(stderr, "Calendar data postfields:\n%s\n", caldata_postfields);
+
+ // fetch event for the cursor date
+ sprintf(uri, "%s%s", GOOGLE_API_URI, calendar_href);
+ fprintf(stderr, "\nCalendar URI: %s\n", uri);
+ char* event = caldav_req(date, uri, "REPORT", caldata_postfields, 0);
+ fprintf(stderr, "Event:\n%s", event);
+ // todo: warn if multiple events,
+ // multistatus has more than just one caldav:calendar-data elements
+
+ // get path of entry
+ char path[100];
+ char* ppath = path;
+ fpath(CONFIG.dir, strlen(CONFIG.dir), date, &ppath, sizeof path);
+ fprintf(stderr, "Cursor date file path: %s\n", path);
+
+ bool local_file_exists = true;
+ bool remote_file_exists = true;
+
+ // check last modification time of local time
+ struct stat attr;
+ if (stat(path, &attr) != 0) {
+ perror("Stat failed");
+ local_file_exists = false;
+ }
+ struct tm* localfile_time = gmtime(&attr.st_mtime);
+ fprintf(stderr, "Local dst: %i\n", localfile_time->tm_isdst);
+ //local_time->tm_isdst = -1;
+ time_t localfile_date = mktime(localfile_time);
+ fprintf(stderr, "Local dst: %i\n", localfile_time->tm_isdst);
+ fprintf(stderr, "Local file last modified time: %s\n", ctime(&localfile_date));
+ fprintf(stderr, "Local file last modified time: %s\n", ctime(&attr.st_mtime));
+
+ struct tm remote_datetime;
+ time_t remote_date;
+
+ // check remote LAST-MODIFIED:20210521T212441Z of remote event
+ char* remote_last_mod = extract_ical_field(event, "LAST-MODIFIED", false);
+ char* remote_uid = extract_ical_field(event, "UID", false);
+ fprintf(stderr, "Remote last modified: %s\n", remote_last_mod);
+ fprintf(stderr, "Remote UID: %s\n", remote_uid);
+ if (remote_last_mod == NULL) {
+ remote_file_exists = false;
+ } else {
+ strptime(remote_last_mod, "%Y%m%dT%H%M%SZ", &remote_datetime);
+ //remote_datetime.tm_isdst = -1;
+ fprintf(stderr, "Remote dst: %i\n", remote_datetime.tm_isdst);
+ remote_date = mktime(&remote_datetime);
+ fprintf(stderr, "Remote dst: %i\n", remote_datetime.tm_isdst);
+ fprintf(stderr, "Remote last modified: %s\n", ctime(&remote_date));
+ }
+
+ if (! (local_file_exists || remote_file_exists)) {
+ fprintf(stderr, "Neither local nor remote file exists, giving up.\n");
+ pthread_cancel(progress_tid);
+ wclear(header);
+ return 0;
+ }
+
+ double timediff = difftime(localfile_date, remote_date);
+ fprintf(stderr, "Time diff between local and remote mod time:%e\n", timediff);
+
+ if ((timediff > 0 && local_file_exists) || (local_file_exists && !remote_file_exists)) {
+ // local time > remote time
+ // if local file mod time more recent than LAST-MODIFIED
+
+ if (remote_file_exists) {
+ // purge any existing daily calendar entries on the remote side
+ char event_uri[300];
+ sprintf(event_uri, "%s%s%s.ics", GOOGLE_API_URI, calendar_href, remote_uid);
+
+ caldav_req(date, event_uri, "DELETE", "", 0);
+ }
+
+ fputs("Local file is newer, uploading to remote...\n", stderr);
+ put_event(date, dir, dir_size, uri);
+
+ pthread_cancel(progress_tid);
+ wclear(header);
+
+ }
+
+ char* rmt_desc;
+ char dstr[16];
+ int conf_ch;
+ if ((timediff < 0 && remote_file_exists) || (!local_file_exists && remote_file_exists)) {
+ rmt_desc = extract_ical_field(event, "DESCRIPTION", true);
+ fprintf(stderr, "Remote event description:%s\n", rmt_desc);
+
+ if (rmt_desc == NULL) {
+ fprintf(stderr, "Could not fetch description of remote event.\n");
+ pthread_cancel(progress_tid);
+ wclear(header);
+ return -1;
+ }
+
+ if (confirm) {
+ // prepare header for confirmation dialogue
+ curs_set(2);
+ noecho();
+ pthread_cancel(progress_tid);
+ wclear(header);
+ }
+
+ // ask for confirmation
+ strftime(dstr, sizeof dstr, CONFIG.fmt, date);
+ mvwprintw(header, 0, 0, "Remote event is more recent. Sync entry '%s' and overwrite local file? [(Y)es/(a)all/(n)o/(c)ancel] ", dstr);
+ bool conf = false;
+ while (!conf) {
+ conf_ch = wgetch(header);
+ if (conf_ch == 'y' || conf_ch == 'Y' || 'a' || conf_ch == '\n' || !confirm) {
+ fprintf(stderr, "Remote file is newer, extracting description from remote...\n");
+
+ // persist downloaded buffer to local file
+ FILE* cursordate_file = fopen(path, "wb");
+ if (cursordate_file == NULL) {
+ perror("Failed to open cursor date file");
+ } else {
+ for (char* i = rmt_desc; *i != '\0'; i++) {
+ if (rmt_desc[i-rmt_desc] == 0x5C) { // backslash
+ switch (*(i+1)) {
+ case 'n':
+ fputc('\n', cursordate_file);
+ i++;
+ break;
+ case 0x5c: // preserve real backslash
+ fputc(0x5c, cursordate_file);
+ i++;
+ break;
+ }
+ } else {
+ fputc(*i, cursordate_file);
+ }
+ }
+ }
+ fclose(cursordate_file);
+
+ // add new entry highlight
+ chtype atrs = winch(cal) & A_ATTRIBUTES;
+ wchgat(cal, 2, atrs | A_BOLD, 0, NULL);
+ prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH, LINES - 1, ASIDE_WIDTH + CAL_WIDTH);
+ }
+ break;
+ }
+
+ echo();
+ curs_set(0);
+ free(rmt_desc);
+ }
+ return conf_ch;
+}
diff --git a/src/caldav.h b/src/caldav.h
@@ -0,0 +1,54 @@
+#ifndef DIARY_CALDAV_H
+#define DIARY_CALDAV_H
+
+#define __USE_XOPEN
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <time.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/poll.h>
+#include <netdb.h>
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <errno.h>
+#include <ncurses.h>
+#include <curl/curl.h>
+#include "utils.h"
+
+#define STR(s) #s
+#define MKSTR(s) STR(s)
+
+// https://developers.google.com/identity/protocols/oauth2/native-app#create-code-challenge
+// A valid code_verifier has a length between 43 and 128 characters
+#define GOOGLE_OAUTH_CODE_VERIFIER_LENGTH 43
+#define GOOGLE_OAUTH_AUTHZ_URL "https://accounts.google.com/o/oauth2/auth"
+#define GOOGLE_OAUTH_TOKEN_URL "https://oauth2.googleapis.com/token"
+#define GOOGLE_OAUTH_SCOPE "https://www.googleapis.com/auth/calendar%20https://www.googleapis.com/auth/calendar.events.owned"
+#define GOOGLE_OAUTH_RESPONSE_TYPE "code"
+#define GOOGLE_OAUTH_REDIRECT_HOST "127.0.0.1"
+#define GOOGLE_OAUTH_REDIRECT_PORT 9004
+#define GOOGLE_OAUTH_REDIRECT_URI "http://" GOOGLE_OAUTH_REDIRECT_HOST ":" MKSTR(GOOGLE_OAUTH_REDIRECT_PORT)
+#define GOOGLE_OAUTH_REDIRECT_SOCKET_BACKLOG 10
+#define GOOGLE_API_URI "https://apidata.googleusercontent.com"
+#define GOOGLE_CALDAV_URI GOOGLE_API_URI "/caldav/v2"
+
+int caldav_sync(struct tm* date,
+ WINDOW* header,
+ WINDOW* cal,
+ int pad_pos,
+ const char* dir,
+ size_t dir_size,
+ bool confirm);
+
+struct curl_mem_chunk {
+ char* memory;
+ size_t size;
+};
+
+#endif
diff --git a/src/diary.c b/src/diary.c
@@ -0,0 +1,687 @@
+#include "diary.h"
+
+int cy, cx;
+time_t raw_time;
+struct tm today;
+struct tm curs_date;
+struct tm cal_start;
+struct tm cal_end;
+
+// normally leap is every 4 years,
+// but is skipped every 100 years,
+// unless it is divisible by 400
+#define is_leap(yr) ((yr % 400 == 0) || (yr % 4 == 0 && yr % 100 != 0))
+
+void setup_cal_timeframe() {
+ raw_time = time(NULL);
+ localtime_r(&raw_time, &today);
+ curs_date = today;
+
+ cal_start = today;
+ cal_start.tm_year -= CONFIG.range;
+ cal_start.tm_mon = 0;
+ cal_start.tm_mday = 1;
+ mktime(&cal_start);
+
+ if (cal_start.tm_wday != CONFIG.weekday) {
+ // adjust start date to weekday before 01.01
+ cal_start.tm_year--;
+ cal_start.tm_mon = 11;
+ cal_start.tm_mday = 31 - (cal_start.tm_wday - CONFIG.weekday) + 1;
+ mktime(&cal_start);
+ }
+
+ cal_end = today;
+ cal_end.tm_year += CONFIG.range;
+ cal_end.tm_mon = 11;
+ cal_end.tm_mday = 31;
+ mktime(&cal_end);
+}
+
+void draw_wdays(WINDOW* head) {
+ int i;
+ for (i = CONFIG.weekday; i < CONFIG.weekday + 7; i++) {
+ waddstr(head, WEEKDAYS[i % 7]);
+ waddch(head, ' ');
+ }
+ wrefresh(head);
+}
+
+void draw_calendar(WINDOW* number_pad, WINDOW* month_pad, const char* diary_dir, size_t diary_dir_size) {
+ struct tm i = cal_start;
+ char month[10];
+ bool has_entry;
+
+ while (mktime(&i) <= mktime(&cal_end)) {
+ has_entry = date_has_entry(diary_dir, diary_dir_size, &i);
+
+ if (has_entry)
+ wattron(number_pad, A_BOLD);
+
+ wprintw(number_pad, "%2i", i.tm_mday);
+
+ if (has_entry)
+ wattroff(number_pad, A_BOLD);
+
+ waddch(number_pad, ' ');
+
+ // print month in sidebar
+ if (i.tm_mday == 1) {
+ strftime(month, sizeof month, "%b", &i);
+ getyx(number_pad, cy, cx);
+ mvwprintw(month_pad, cy, 0, "%s ", month);
+ }
+
+ i.tm_mday++;
+ mktime(&i);
+ }
+}
+
+bool go_to(WINDOW* calendar, WINDOW* aside, time_t date, int* cur_pad_pos) {
+ if (date < mktime(&cal_start) || date > mktime(&cal_end))
+ return false;
+
+ int diff_seconds = date - mktime(&cal_start);
+ int diff_days = diff_seconds / 60 / 60 / 24;
+ int diff_weeks = diff_days / 7;
+ int diff_wdays = diff_days % 7;
+
+ localtime_r(&date, &curs_date);
+
+ getyx(calendar, cy, cx);
+
+ // remove the STANDOUT attribute from the day we are leaving
+ chtype current_attrs = mvwinch(calendar, cy, cx) & A_ATTRIBUTES;
+ // leave every attr as is, but turn off STANDOUT
+ current_attrs &= ~A_STANDOUT;
+ mvwchgat(calendar, cy, cx, 2, current_attrs, 0, NULL);
+
+ // add the STANDOUT attribute to the day we are entering
+ chtype new_attrs = mvwinch(calendar, diff_weeks, diff_wdays * 3) & A_ATTRIBUTES;
+ new_attrs |= A_STANDOUT;
+ mvwchgat(calendar, diff_weeks, diff_wdays * 3, 2, new_attrs, 0, NULL);
+
+ if (diff_weeks < *cur_pad_pos)
+ *cur_pad_pos = diff_weeks;
+ if (diff_weeks > *cur_pad_pos + LINES - 2)
+ *cur_pad_pos = diff_weeks - LINES + 2;
+ prefresh(aside, *cur_pad_pos, 0, 1, 0, LINES - 1, ASIDE_WIDTH);
+ prefresh(calendar, *cur_pad_pos, 0, 1, ASIDE_WIDTH, LINES - 1, ASIDE_WIDTH + CAL_WIDTH);
+
+ return true;
+}
+
+/* Update window 'win' with diary entry from date 'date' */
+void display_entry(const char* dir, size_t dir_size, const struct tm* date, WINDOW* win, int width) {
+ char path[100];
+ char* ppath = path;
+ int c;
+
+ // get entry path
+ fpath(dir, dir_size, date, &ppath, sizeof path);
+ if (ppath == NULL) {
+ fprintf(stderr, "Error while retrieving file path for diary reading");
+ return;
+ }
+
+ wclear(win);
+
+ if (date_has_entry(dir, dir_size, date)) {
+ FILE* fp = fopen(path, "r");
+ if (fp == NULL) perror("Error opening file");
+
+ wmove(win, 0, 0);
+ while((c = getc(fp)) != EOF) waddch(win, c);
+
+ fclose(fp);
+ }
+
+ wrefresh(win);
+}
+
+/* Writes edit command for 'date' entry to 'rcmd'. '*rcmd' is NULL on error. */
+void edit_cmd(const char* dir, size_t dir_size, const struct tm* date, char** rcmd, size_t rcmd_size) {
+ // set the edit command to env editor
+ if (strlen(CONFIG.editor) + 2 > rcmd_size) {
+ fprintf(stderr, "Binary path of default editor too long");
+ *rcmd = NULL;
+ return;
+ }
+ strcpy(*rcmd, CONFIG.editor);
+ strcat(*rcmd, " ");
+
+ // get path of entry
+ char path[100];
+ char* ppath = path;
+ fpath(dir, dir_size, date, &ppath, sizeof path);
+
+ if (ppath == NULL) {
+ fprintf(stderr, "Error while retrieving file path for editing");
+ *rcmd = NULL;
+ return;
+ }
+
+ // concatenate editor command with entry path
+ if (strlen(*rcmd) + strlen(path) + 1 > rcmd_size) {
+ fprintf(stderr, "Edit command too long");
+ return;
+ }
+ strcat(*rcmd, path);
+}
+
+bool date_has_entry(const char* dir, size_t dir_size, const struct tm* i) {
+ char epath[100];
+ char* pepath = epath;
+
+ // get entry path and check for existence
+ fpath(dir, dir_size, i, &pepath, sizeof epath);
+
+ if (pepath == NULL) {
+ fprintf(stderr, "Error while retrieving file path for checking entry existence");
+ return false;
+ }
+
+ return (access(epath, F_OK) != -1);
+}
+
+/*
+ * Finds the date with a diary entry closest to <current>.
+ * Depending on <search_backwards>, it will find the most recent
+ * previous date or the oldest next date with an entry. If no
+ * entry is found within the calendar size, <current> is returned.
+ */
+struct tm find_closest_entry(const struct tm current,
+ bool search_backwards,
+ const char* diary_dir,
+ size_t diary_dir_size) {
+ time_t end_time = mktime(&cal_end);
+ time_t start_time = mktime(&cal_start);
+
+ int step = search_backwards ? -1 : +1;
+
+ struct tm it = current;
+ it.tm_mday += step;
+ time_t it_time = mktime(&it);
+
+ for( ; it_time >= start_time && it_time <= end_time; it_time = mktime(&it)) {
+ if (date_has_entry(diary_dir, diary_dir_size, &it)) {
+ return it;
+ }
+ it.tm_mday += step;
+ }
+
+ return current;
+}
+
+bool read_config(const char* file_path) {
+ char* expaned_value;
+ char config_file_path[256];
+
+ expaned_value = expand_path(file_path);
+ strcpy(config_file_path, expaned_value);
+ free(expaned_value);
+
+ // check if config file is readable
+ if( access( config_file_path, R_OK ) != 0 ) {
+ fprintf(stderr, "Config file '%s' not readable, skipping\n", config_file_path);
+ return false;
+ }
+
+ char key_buf[80];
+ char value_buf[80];
+ char line[256];
+ FILE * pfile;
+
+ // read config file line by line
+ pfile = fopen(config_file_path, "r");
+ while (fgets(line, sizeof line, pfile)) {
+ // ignore comment lines
+ if (*line == '#' || *line == ';') continue;
+
+ if (sscanf(line, "%s = %s", key_buf, value_buf) == 2) {
+ if (strcmp("dir", key_buf) == 0) {
+ expaned_value = expand_path(value_buf);
+ strcpy(CONFIG.dir, expaned_value);
+ free(expaned_value);
+ } else if (strcmp("range", key_buf) == 0) {
+ CONFIG.range = atoi(value_buf);
+ } else if (strcmp("weekday", key_buf) == 0) {
+ CONFIG.weekday = atoi(value_buf);
+ } else if (strcmp("fmt", key_buf) == 0) {
+ CONFIG.fmt = (char *) malloc(strlen(value_buf) + 1 * sizeof(char));
+ strcpy(CONFIG.fmt, value_buf);
+ } else if (strcmp("editor", key_buf) == 0) {
+ CONFIG.editor = (char *) malloc(strlen(value_buf) + 1 * sizeof(char));
+ strcpy(CONFIG.editor, value_buf);
+ } else if (strcmp("google_tokenfile", key_buf) == 0) {
+ expaned_value = expand_path(value_buf);
+ strcpy(CONFIG.google_tokenfile, expaned_value);
+ free(expaned_value);
+ } else if (strcmp("google_clientid", key_buf) == 0) {
+ CONFIG.google_clientid = (char *) malloc(strlen(value_buf) + 1 * sizeof(char));
+ strcpy(CONFIG.google_clientid, value_buf);
+ } else if (strcmp("google_secretid", key_buf) == 0) {
+ CONFIG.google_secretid = (char *) malloc(strlen(value_buf) + 1 * sizeof(char));
+ strcpy(CONFIG.google_secretid, value_buf);
+ } else if (strcmp("google_calendar", key_buf) == 0) {
+ CONFIG.google_calendar = (char *) malloc(strlen(value_buf) + 1 * sizeof(char));
+ strcpy(CONFIG.google_calendar, value_buf);
+ }
+ }
+ }
+ fclose (pfile);
+ return true;
+}
+
+void usage() {
+ printf("Usage : diary [OPTION]... [DIRECTORY]...\n");
+ printf("\n");
+ printf("Diary, journaling TUI (v%s)\n", DIARY_VERSION);
+ printf("Edit journal entries from the command line\n");
+ printf("\n");
+ printf("Options:\n");
+ printf(" -v, --version : Print diary version\n");
+ printf(" -h, --help : Show diary help text\n");
+ printf(" -d, --dir DIARY_DIR : Diary storage directory DIARY_DIR\n");
+ printf(" -e, --editor EDITOR : Editor to open journal files with\n");
+ printf(" -f, --fmt FMT : Date and file format, change with care\n");
+ printf(" -r, --range RANGE : RANGE is the number of years to show before/after today's date\n");
+ printf(" -w, --weekday DAY : First day of the week, 0 = Sun, 1 = Mon, ..., 6 = Sat\n");
+ printf("\n");
+ printf("Full docs and keyboard shortcuts: 'man diary'\n");
+ printf("or online via: <https://github.com/in0rdr/diary>\n");
+}
+
+int main(int argc, char** argv) {
+ setlocale(LC_ALL, "");
+ char* env_var;
+ char* config_home;
+ char* config_file_path;
+ chtype atrs;
+
+ // the diary directory defaults to the diary_dir specified in the config file
+ config_home = getenv("XDG_CONFIG_HOME");
+ if (config_home == NULL) config_home = XDG_CONFIG_HOME_FALLBACK;
+ // concat config home with the file path to the config file
+ config_file_path = (char *) calloc(strlen(config_home) + strlen(CONFIG_FILE_PATH) + 1, sizeof(char));
+ sprintf(config_file_path, "%s/%s", config_home, CONFIG_FILE_PATH);
+ // read config from config file path
+ read_config(config_file_path);
+
+ // get diary directory from environment
+ env_var = getenv("DIARY_DIR");
+ if (env_var != NULL) {
+ // if available, overwrite the diary directory with the environment variable
+ CONFIG.dir = (char *) calloc(strlen(env_var) + 1, sizeof(char));
+ strcpy(CONFIG.dir, env_var);
+ }
+
+ // get editor from environment
+ env_var = getenv("EDITOR");
+ if (env_var != NULL) {
+ // if available, overwrite the editor with the environments EDITOR
+ CONFIG.editor = (char *) calloc(strlen(env_var) + 1, sizeof(char));
+ strcpy(CONFIG.editor, env_var);
+ }
+
+ // get locale from environment variable LANG
+ // https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html
+ env_var = getenv("LANG");
+ if (env_var != NULL) {
+ // if available, overwrite the editor with the environments locale
+ #ifdef __GNU_LIBRARY__
+ // references: locale(5) and util-linux's cal.c
+ // get the base date, 8-digit integer (YYYYMMDD) returned as char *
+ #ifdef _NL_TIME_WEEK_1STDAY
+ unsigned long d = (uintptr_t) nl_langinfo(_NL_TIME_WEEK_1STDAY);
+ // reference: https://sourceware.org/glibc/wiki/Locales
+ // assign a static date value 19971130 (a Sunday)
+ #else
+ unsigned long d = 19971130;
+ #endif
+ struct tm base = {
+ .tm_sec = 0,
+ .tm_min = 0,
+ .tm_hour = 0,
+ .tm_mday = d % 100,
+ .tm_mon = (d / 100) % 100 - 1,
+ .tm_year = d / (100 * 100) - 1900
+ };
+ mktime(&base);
+ // weekday is base date's day of the week offset by (_NL_TIME_FIRST_WEEKDAY - 1)
+ #ifdef __linux__
+ CONFIG.weekday = (base.tm_wday + *nl_langinfo(_NL_TIME_FIRST_WEEKDAY) - 1) % 7;
+ #elif defined __MACH__
+ CFIndex first_day_of_week;
+ CFCalendarRef currentCalendar = CFCalendarCopyCurrent();
+ first_day_of_week = CFCalendarGetFirstWeekday(currentCalendar);
+ CFRelease(currentCalendar);
+ CONFIG.weekday = (base.tm_wday + first_day_of_week - 1) % 7;
+ #endif
+ #endif
+ }
+
+ // get the diary directory via argument, this takes precedence over env/config
+ if (argc < 2) {
+ if (CONFIG.dir == NULL) {
+ fprintf(stderr, "The diary directory must be provided as (non-option) arg, `--dir` arg,\n"
+ "or in the DIARY_DIR environment variable, see `diary --help` or DIARY(1)\n");
+ return 1;
+ }
+ } else {
+ int option_char;
+ int option_index = 0;
+
+ // define options, see GETOPT(3)
+ static const struct option long_options[] = {
+ { "version", no_argument, 0, 'v' },
+ { "help", no_argument, 0, 'h' },
+ { "dir", required_argument, 0, 'd' },
+ { "editor", required_argument, 0, 'e' },
+ { "fmt", required_argument, 0, 'f' },
+ { "range", required_argument, 0, 'r' },
+ { "weekday", required_argument, 0, 'w' },
+ { 0, 0, 0, 0 }
+ };
+
+ // read option characters
+ while (1) {
+ option_char = getopt_long(argc, argv, "vhd:r:w:f:e:", long_options, &option_index);
+
+ if (option_char == -1) {
+ break;
+ }
+
+ switch (option_char) {
+ case 'v':
+ // show program version
+ printf("v%s\n", DIARY_VERSION);
+ return 0;
+ break;
+ case 'h':
+ // show help text
+ // printf("see man(1) diary\n");
+ usage();
+ return 0;
+ break;
+ case 'd':
+ // set diary directory from option character
+ CONFIG.dir = (char *) calloc(strlen(optarg) + 1, sizeof(char));
+ strcpy(CONFIG.dir, optarg);
+ break;
+ case 'r':
+ // set year range from option character
+ CONFIG.range = atoi(optarg);
+ break;
+ case 'w':
+ // set first week day from option character
+ fprintf(stderr, "%i\n", atoi(optarg));
+ CONFIG.weekday = atoi(optarg);
+ break;
+ case 'f':
+ // set date format from option character
+ CONFIG.fmt = (char *) calloc(strlen(optarg) + 1, sizeof(char));
+ strcpy(CONFIG.fmt, optarg);
+ break;
+ case 'e':
+ // set default editor from option character
+ CONFIG.editor = (char *) calloc(strlen(optarg) + 1, sizeof(char));
+ strcpy(CONFIG.editor, optarg);
+ break;
+ default:
+ printf("?? getopt returned character code 0%o ??\n", option_char);
+ }
+ }
+
+ if (optind < argc) {
+ // set diary directory from first non-option argv-element,
+ // required for backwarad compatibility with diary <= 0.4
+ CONFIG.dir = (char *) calloc(strlen(argv[optind]) + 1, sizeof(char));
+ strcpy(CONFIG.dir, argv[optind]);
+ }
+ }
+
+ // check if that directory exists
+ DIR* diary_dir_ptr = opendir(CONFIG.dir);
+ if (diary_dir_ptr) {
+ // directory exists, continue
+ closedir(diary_dir_ptr);
+ } else if (errno == ENOENT) {
+ fprintf(stderr, "The directory '%s' does not exist\n", CONFIG.dir);
+ return 2;
+ } else {
+ fprintf(stderr, "The directory '%s' could not be opened\n", CONFIG.dir);
+ return 1;
+ }
+
+ setup_cal_timeframe();
+
+ initscr();
+ raw();
+ curs_set(0);
+
+ WINDOW* header = newwin(1, COLS - CAL_WIDTH - ASIDE_WIDTH, 0, ASIDE_WIDTH + CAL_WIDTH);
+ wattron(header, A_BOLD);
+ update_date(header, &curs_date);
+ WINDOW* wdays = newwin(1, 3 * 7, 0, ASIDE_WIDTH);
+ draw_wdays(wdays);
+
+ WINDOW* aside = newpad((CONFIG.range * 2 + 1) * 12 * MAX_MONTH_HEIGHT, ASIDE_WIDTH);
+ WINDOW* cal = newpad((CONFIG.range * 2 + 1) * 12 * MAX_MONTH_HEIGHT, CAL_WIDTH);
+ keypad(cal, TRUE);
+ draw_calendar(cal, aside, CONFIG.dir, strlen(CONFIG.dir));
+
+ int ch, conf_ch;
+ int pad_pos = 0;
+ int syear = 0, smonth = 0, sday = 0;
+ struct tm new_date;
+ int prev_width = COLS - ASIDE_WIDTH - CAL_WIDTH;
+ int prev_height = LINES - 1;
+ size_t diary_dir_size = strlen(CONFIG.dir);
+
+ bool mv_valid = go_to(cal, aside, raw_time, &pad_pos);
+ // mark current day
+ atrs = winch(cal) & A_ATTRIBUTES;
+ wchgat(cal, 2, atrs | A_UNDERLINE, 0, NULL);
+ prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH, LINES - 1, ASIDE_WIDTH + CAL_WIDTH);
+
+ WINDOW* prev = newwin(prev_height, prev_width, 1, ASIDE_WIDTH + CAL_WIDTH);
+ display_entry(CONFIG.dir, diary_dir_size, &today, prev, prev_width);
+
+
+ do {
+ ch = wgetch(cal);
+ // new_date represents the desired date the user wants to go_to(),
+ // which may not be a feasible date at all
+ new_date = curs_date;
+ char ecmd[150];
+ char* pecmd = ecmd;
+ char pth[100];
+ char* ppth = pth;
+ char dstr[16];
+ time_t end_time = mktime(&cal_end);
+ struct tm it = cal_start;
+ time_t it_time = mktime(&it);
+ edit_cmd(CONFIG.dir, diary_dir_size, &new_date, &pecmd, sizeof ecmd);
+
+ switch(ch) {
+ // basic movements
+ case 'j':
+ case KEY_DOWN:
+ new_date.tm_mday += 7;
+ mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
+ break;
+ case 'k':
+ case KEY_UP:
+ new_date.tm_mday -= 7;
+ mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
+ break;
+ case 'l':
+ case KEY_RIGHT:
+ new_date.tm_mday++;
+ mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
+ break;
+ case 'h':
+ case KEY_LEFT:
+ new_date.tm_mday--;
+ mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
+ break;
+
+ // jump to top/bottom of page
+ case 'g':
+ new_date = find_closest_entry(cal_start, false, CONFIG.dir, diary_dir_size);
+ mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
+ break;
+ case 'G':
+ new_date = find_closest_entry(cal_end, true, CONFIG.dir, diary_dir_size);
+ mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
+ break;
+
+ // jump backward/forward by a month
+ case 'K':
+ if (new_date.tm_mday == 1)
+ new_date.tm_mon--;
+ new_date.tm_mday = 1;
+ mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
+ break;
+ case 'J':
+ new_date.tm_mon++;
+ new_date.tm_mday = 1;
+ mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
+ break;
+
+ // find specific date
+ case 'f':
+ wclear(header);
+ curs_set(2);
+ mvwprintw(header, 0, 0, "Go to date [YYYY-MM-DD]: ");
+ if (wscanw(header, "%4i-%2i-%2i", &syear, &smonth, &sday) == 3) {
+ // struct tm.tm_year: years since 1900
+ new_date.tm_year = syear - 1900;
+ // struct tm.tm_mon in range [0, 11]
+ new_date.tm_mon = smonth - 1;
+ new_date.tm_mday = sday;
+ mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
+ }
+ curs_set(0);
+ break;
+ // today shortcut
+ case 't':
+ new_date = today;
+ mv_valid = go_to(cal, aside, raw_time, &pad_pos);
+ break;
+ // delete entry
+ case 'd':
+ case 'x':
+ if (date_has_entry(CONFIG.dir, diary_dir_size, &curs_date)) {
+ // get file path of entry and delete entry
+ fpath(CONFIG.dir, diary_dir_size, &curs_date, &ppth, sizeof pth);
+ if (ppth == NULL) {
+ fprintf(stderr, "Error retrieving file path for entry removal");
+ break;
+ }
+
+ // prepare header for confirmation dialogue
+ wclear(header);
+ curs_set(2);
+ noecho();
+
+ // ask for confirmation
+ strftime(dstr, sizeof dstr, CONFIG.fmt, &curs_date);
+ mvwprintw(header, 0, 0, "Delete entry '%s'? [Y/n] ", dstr);
+ bool conf = false;
+ while (!conf) {
+ conf_ch = wgetch(header);
+ if (conf_ch == 'y' || conf_ch == 'Y' || conf_ch == '\n') {
+ if (unlink(pth) != -1) {
+ // file successfully deleted, remove entry highlight
+ atrs = winch(cal) & A_ATTRIBUTES;
+ wchgat(cal, 2, atrs & ~A_BOLD, 0, NULL);
+ prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH,
+ LINES - 1, ASIDE_WIDTH + CAL_WIDTH);
+ }
+ } else if (conf_ch == 27 || conf_ch == 'n') {
+ update_date(header, &curs_date);
+ }
+ break;
+ }
+
+ echo();
+ curs_set(0);
+ }
+ break;
+ // edit/create a diary entry
+ case 'e':
+ case '\n':
+ if (pecmd == NULL) {
+ fprintf(stderr, "Error retrieving edit command");
+ break;
+ }
+ curs_set(1);
+ system(ecmd);
+ curs_set(0);
+ keypad(cal, TRUE);
+
+ // mark newly created entry
+ if (date_has_entry(CONFIG.dir, diary_dir_size, &curs_date)) {
+ atrs = winch(cal) & A_ATTRIBUTES;
+ wchgat(cal, 2, atrs | A_BOLD, 0, NULL);
+
+ // refresh the calendar to add highlighting
+ prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH,
+ LINES - 1, ASIDE_WIDTH + CAL_WIDTH);
+ }
+ break;
+ // Move to the previous diary entry
+ case 'N':
+ new_date = find_closest_entry(new_date, true, CONFIG.dir, diary_dir_size);
+ mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
+ break;
+ // Move to the next diary entry
+ case 'n':
+ new_date = find_closest_entry(new_date, false, CONFIG.dir, diary_dir_size);
+ mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos);
+ break;
+ // Sync entry with CalDAV server.
+ // Show confirmation dialogue before overwriting local files
+ case 's':
+ caldav_sync(&curs_date, header, cal, pad_pos, CONFIG.dir, diary_dir_size, true);
+ break;
+ // Sync all entries with CalDAV server;
+ case 'S':
+ for( ; it_time <= end_time; it_time = mktime(&it)) {
+ if (conf_ch == -1) {
+ // sync error
+ break;
+ } else if (conf_ch == 'c') {
+ // cancel all
+ break;
+ } else if (conf_ch == 'a') {
+ // yes to (a)ll
+ conf_ch = caldav_sync(&it, header, cal, pad_pos, CONFIG.dir, diary_dir_size, false);
+ } else {
+ // show confirmation dialogue before overwriting local files
+ conf_ch = caldav_sync(&it, header, cal, pad_pos, CONFIG.dir, diary_dir_size, true);
+ }
+ it.tm_mday++;
+ }
+ break;
+ }
+
+ if (mv_valid) {
+ update_date(header, &curs_date);
+
+ // adjust prev and header width (if terminal was resized in the mean time)
+ prev_width = COLS - ASIDE_WIDTH - CAL_WIDTH;
+ wresize(prev, prev_height, prev_width);
+ wresize(header, 1, prev_width);
+
+ // read the diary
+ display_entry(CONFIG.dir, diary_dir_size, &curs_date, prev, prev_width);
+ }
+ } while (ch != 'q');
+
+ endwin();
+ system("clear");
+ return 0;
+}
diff --git a/src/diary.h b/src/diary.h
@@ -0,0 +1,39 @@
+#ifndef DIARY_H
+#define DIARY_H
+
+
+#ifdef __MACH__
+ #include <CoreFoundation/CoreFoundation.h>
+#endif
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <string.h>
+#include <time.h>
+#include <errno.h>
+#include <dirent.h>
+#include <ncurses.h>
+#include <locale.h>
+#include <langinfo.h>
+#include "utils.h"
+#include "caldav.h"
+
+#define XDG_CONFIG_HOME_FALLBACK "~/.config"
+#define CONFIG_FILE_PATH "diary/diary.cfg"
+#define DIARY_VERSION "0.6-unstable"
+
+static const char* WEEKDAYS[] = {"Su","Mo","Tu","We","Th","Fr","Sa"};
+
+void setup_cal_timeframe();
+void draw_wdays(WINDOW* head);
+void draw_calendar(WINDOW* number_pad, WINDOW* month_pad, const char* diary_dir, size_t diary_dir_size);
+
+bool go_to(WINDOW* calendar, WINDOW* aside, time_t date, int* cur_pad_pos);
+void display_entry(const char* dir, size_t dir_size, const struct tm* date, WINDOW* win, int width);
+void edit_cmd(const char* dir, size_t dir_size, const struct tm* date, char** rcmd, size_t rcmd_size);
+
+bool date_has_entry(const char* dir, size_t dir_size, const struct tm* i);
+
+#endif
diff --git a/src/utils.c b/src/utils.c
@@ -0,0 +1,335 @@
+#include "utils.h"
+
+/* Update the header with the cursor date */
+void update_date(WINDOW* header, struct tm* curs_date) {
+ // TODO: dstr for strlen(CONFIG.format) > 16 ?
+ char dstr[16];
+ mktime(curs_date);
+ strftime(dstr, sizeof dstr, CONFIG.fmt, curs_date);
+
+ wclear(header);
+ mvwaddstr(header, 0, 0, dstr);
+ wrefresh(header);
+}
+
+
+char* extract_json_value(const char* json, char* key, bool quoted) {
+ // work on a copy of the json
+ char* jsoncp = (char*) malloc(strlen(json) * sizeof(char) + 1);
+ if (jsoncp == NULL) {
+ perror("malloc failed");
+ return NULL;
+ }
+ strcpy(jsoncp, json);
+
+ char* tok = strtok(jsoncp, " ");
+ while (tok != NULL) {
+ if (strstr(tok, key) != NULL) {
+ tok = strtok(NULL, " "); // value
+ break;
+ }
+ // key was not in this tok, advance tok
+ tok = strtok(NULL, " ");
+ }
+
+ // remove quotes and comma or commma only
+ if (quoted) {
+ tok = strtok(tok, "\"");
+ } else {
+ tok = strtok(tok, ",");
+ }
+
+ char* res = NULL;
+ if (tok != NULL) {
+ res = (char*) malloc(strlen(tok) * sizeof(char) + 1);
+ if (res == NULL) {
+ perror("malloc failed");
+ return NULL;
+ }
+ strcpy(res, tok);
+ }
+
+ free(jsoncp);
+ return res;
+}
+
+char* fold(const char* str) {
+ // work on a copy of the str
+ char* strcp = (char *) malloc(strlen(str) * sizeof(char) + 1);
+ if (strcp == NULL) {
+ perror("malloc failed");
+ return NULL;
+ }
+ strcpy(strcp, str);
+
+ // create buffer for escaped result TEXT
+ char* buf = malloc(1);
+ if (buf == NULL) {
+ perror("malloc failed");
+ return NULL;
+ }
+ buf[0] = '\0';
+
+ void* newbuf;
+ // bufl is the current buffer size incl. \0
+ int bufl = 1;
+ // i is the iterator in strcp
+ char* i = strcp;
+ // escch is the char to be escaped,
+ // only written when esc=true
+ char escch;
+ bool esc = false;
+
+ while(*i != '\0' || esc) {
+ fprintf(stderr, "strlen(buf): %i\n", bufl);
+ fprintf(stderr, "*i: %c\n", *i);
+ fprintf(stderr, "escch: %c\n", escch);
+ fprintf(stderr, "esc: %i\n", esc);
+ fprintf(stderr, "buffer: %s\n\n", buf);
+
+ newbuf = realloc(buf, ++bufl);
+ if (newbuf == NULL) {
+ perror("realloc failed");
+ free(buf);
+ return NULL;
+ }
+ buf = (char*) newbuf;
+
+ if ((bufl > 1) && ((bufl % 77) == 0)) {
+ // break lines after 75 chars
+ // split between any two characters by inserting a CRLF
+ // immediately followed by a white space character
+ buf[bufl-2] = '\n';
+ escch = ' ';
+ esc = true;
+ continue;
+ }
+
+ if (esc) {
+ // only escape char, do not advance iterator i
+ buf[bufl-2] = escch;
+ esc = false;
+ } else {
+ // escape characters
+ // https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11
+ switch (*i) {
+ case 0x5c: // backslash
+ buf[bufl-2] = 0x5c;
+ escch = 0x5c;
+ esc = true;
+ break;
+ case ';':
+ buf[bufl-2] = 0x5c;
+ escch = ';';
+ esc = true;
+ break;
+ case ',':
+ buf[bufl-2] = 0x5c;
+ escch = ',';
+ esc = true;
+ break;
+ case '\n':
+ buf[bufl-2] = 0x5c;
+ escch = 'n';
+ esc = true;
+ break;
+ default:
+ // write regular character from strcp
+ buf[bufl-2] = *i;
+ break;
+ }
+ i++;
+ }
+
+ // terminate the char string in any case (esc or not)
+ buf[bufl-1] = '\0';
+ }
+
+ fprintf(stderr, "escch: %c\n", escch);
+ fprintf(stderr, "end: %c\n", buf[bufl]);
+
+ free(strcp);
+ return buf;
+}
+
+char* unfold(const char* str) {
+ fprintf(stderr, "Before unfolding: %s\n", str);
+ //if (strcmp(str, "")) {
+ // fputs("Unfold string is empty.\n", stderr);
+ // return NULL;
+ //}
+
+ // work on a copy of the str
+ char* strcp = (char *) malloc(strlen(str) * sizeof(char) + 1);
+ if (strcp == NULL) {
+ perror("malloc failed");
+ return NULL;
+ }
+ strcpy(strcp, str);
+
+ char* res = strtok(strcp, "\n");
+
+ char* buf = malloc(strlen(res) + 1);
+ if (buf == NULL) {
+ perror("malloc failed");
+ return NULL;
+ }
+ strcpy(buf, res);
+
+ char* newbuf;
+ regex_t re;
+ regmatch_t pm[1];
+
+ if (regcomp(&re, "^[^ \t]", 0) != 0) {
+ perror("Failed to compile regex");
+ return NULL;
+ }
+
+ while (res != NULL) {
+ res = strtok(NULL, "\n");
+
+ if (regexec(&re, res, 1, pm, 0) == 0) {
+ // Stop unfolding if line does not start with white space/tab:
+ // https://datatracker.ietf.org/doc/html/rfc2445#section-4.1
+ break;
+ }
+
+ newbuf = realloc(buf, strlen(buf) + strlen(res) + 1);
+ if (buf == NULL) {
+ perror("realloc failed");
+ free(buf);
+ return NULL;
+ } else {
+ buf = newbuf;
+ strcat(buf, res + 1);
+ }
+ }
+
+ regfree(&re);
+ free(strcp);
+ return buf;
+}
+
+char* extract_ical_field(const char* ics, char* key, bool multiline) {
+ regex_t re;
+ regmatch_t pm[1];
+ char key_regex[strlen(key) + 1];
+ sprintf(key_regex, "^%s", key);
+ fprintf(stderr, "Key regex: %s\n", key_regex);
+
+ if (regcomp(&re, key_regex, 0) != 0) {
+ perror("Failed to compile regex");
+ return NULL;
+ }
+
+ // work on a copy of the ical xml response
+ char* icscp = (char *) malloc(strlen(ics) * sizeof(char) + 1);
+ if (icscp == NULL) {
+ perror("malloc failed");
+ return NULL;
+ }
+ strcpy(icscp, ics);
+
+ // tokenize ical by newlines
+ char* res = strtok(icscp, "\n");
+
+ while (res != NULL) {
+ if (regexec(&re, res, 1, pm, 0) == 0) {
+ res = strstr(res, ":"); // value
+ res++; // strip the ":"
+
+ fprintf(stderr, "Extracted ical result value: %s\n", res);
+ fprintf(stderr, "Extracted ical result size: %li\n", strlen(res));
+
+ if (strlen(res) == 0) {
+ // empty remote description
+ res = NULL;
+ } else if (multiline) {
+ res = unfold(ics + (res - icscp));
+ }
+ break;
+ }
+ // key not in this line, advance line
+ res = strtok(NULL, "\n");
+ }
+ fprintf(stderr, "Sizeof ics: %li\n", strlen(ics));
+
+ free(icscp);
+ return res;
+}
+
+// Return expanded file path
+char* expand_path(const char* str) {
+ char* res;
+ wordexp_t str_wordexp;
+ if ( wordexp( str, &str_wordexp, 0 ) == 0) {
+ res = (char *) calloc(strlen(str_wordexp.we_wordv[0]) + 1, sizeof(char));
+ strcpy(res, str_wordexp.we_wordv[0]);
+ }
+ wordfree(&str_wordexp);
+ return res;
+}
+
+// Get last occurence of string in string
+// https://stackoverflow.com/questions/20213799/finding-last-occurence-of-string
+char* strrstr(char *haystack, char *needle) {
+ int nlen = strlen(needle);
+ for (char* i = haystack + strlen(haystack) - nlen; i >= haystack; i--) {
+ if (strncmp(i, needle, nlen) == 0) {
+ return i;
+ }
+ }
+ return NULL;
+}
+
+/* Writes file path for 'date' entry to 'rpath'. '*rpath' is NULL on error. */
+void fpath(const char* dir, size_t dir_size, const struct tm* date, char** rpath, size_t rpath_size)
+{
+ // check size of result path
+ if (dir_size + 1 > rpath_size) {
+ fprintf(stderr, "Directory path too long");
+ *rpath = NULL;
+ return;
+ }
+
+ // add path of the diary dir to result path
+ strcpy(*rpath, dir);
+
+ // check for terminating '/' in path
+ if (dir[dir_size - 1] != '/') {
+ // check size again to accommodate '/'
+ if (dir_size + 1 > rpath_size) {
+ fprintf(stderr, "Directory path too long");
+ *rpath = NULL;
+ return;
+ }
+ strcat(*rpath, "/");
+ }
+
+ char dstr[16];
+ strftime(dstr, sizeof dstr, CONFIG.fmt, date);
+
+ // append date to the result path
+ if (strlen(*rpath) + strlen(dstr) > rpath_size) {
+ fprintf(stderr, "File path too long");
+ *rpath = NULL;
+ return;
+ }
+ strcat(*rpath, dstr);
+}
+
+// TODO: write functions for (un)escaped TEXT
+// https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11
+char* unescape_ical_description(const char* description);
+char* escape_ical_description(const char* description);
+
+config CONFIG = {
+ .range = 1,
+ .weekday = 1,
+ .fmt = "%Y-%m-%d",
+ .editor = "",
+ .google_tokenfile = GOOGLE_OAUTH_TOKEN_FILE,
+ .google_clientid = GOOGLE_OAUTH_CLIENT_ID,
+ .google_secretid = GOOGLE_OAUTH_CLIENT_SECRET,
+ .google_calendar = ""
+};
diff --git a/src/utils.h b/src/utils.h
@@ -0,0 +1,58 @@
+#ifndef DIARY_UTILS_H
+#define DIARY_UTILS_H
+
+#include <regex.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+#include <string.h>
+#include <wordexp.h>
+#include <stdbool.h>
+#include <ncurses.h>
+
+#define GOOGLE_OAUTH_TOKEN_FILE "~/.diary-token"
+#ifndef GOOGLE_OAUTH_CLIENT_ID
+ #define GOOGLE_OAUTH_CLIENT_ID ""
+#endif
+#ifndef GOOGLE_OAUTH_CLIENT_SECRET
+ #define GOOGLE_OAUTH_CLIENT_SECRET ""
+#endif
+
+#define CAL_WIDTH 21
+#define ASIDE_WIDTH 4
+#define MAX_MONTH_HEIGHT 6
+
+void update_date(WINDOW* header, struct tm* curs_date);
+char* extract_json_value(const char* json, char* key, bool quoted);
+char* fold(const char* str);
+char* unfold(const char* str);
+char* extract_ical_field(const char* ical, char* key, bool multline);
+char* expand_path(const char* str);
+char* strrstr(char *haystack, char *needle);
+void fpath(const char* dir, size_t dir_size, const struct tm* date, char** rpath, size_t rpath_size);
+
+typedef struct
+{
+ // Path that holds the journal text files
+ char* dir;
+ // Number of years to show before/after todays date
+ int range;
+ // 7 = Sunday, 1 = Monday, ..., 6 = Saturday
+ int weekday;
+ // 2020-12-31
+ char* fmt;
+ // Editor to open journal files with
+ char* editor;
+ // File for Google OAuth access token
+ char* google_tokenfile;
+ // Google client id
+ char* google_clientid;
+ // Google secret id
+ char* google_secretid;
+ // Google calendar to synchronize
+ char* google_calendar;
+} config;
+
+config CONFIG;
+
+#endif