diary

Text-based journaling program
git clone https://git.in0rdr.ch/diary.git
Log | Files | Refs | README | LICENSE

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+
MMakefile | 16++++++++++------
MREADME.md | 91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
ATESTING.md | 33+++++++++++++++++++++++++++++++++
Aconfig/diary.cfg | 17+++++++++++++++++
Ddiary-cheat-sheet.png | 0
Ddiary.1 | 192-------------------------------------------------------------------------------
Ddiary.c | 719-------------------------------------------------------------------------------
Ddiary.cfg | 11-----------
Ddiary.h | 65-----------------------------------------------------------------
Rdemo.gif -> img/demo.gif | 0
Aimg/diary-cheat-sheet.png | 0
Aimg/diary-worm.png | 0
Aimg/diary-worm.svg | 1209+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aman/diary.1 | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aman/diary.1.html | 319+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/caldav.c | 877+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/caldav.h | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/diary.c | 687+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/diary.h | 39+++++++++++++++++++++++++++++++++++++++
Asrc/utils.c | 335+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/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>&#x00A0;</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 &lt;&lt;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: + &lt;https://github.com/in0rdr/diary&gt;. 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