Compare commits
101 Commits
6973fb9f04
...
main
Author | SHA1 | Date | |
---|---|---|---|
5330864ae5 | |||
9d840d95d2 | |||
a5942d2860 | |||
d7dc02979b | |||
96b1c76137 | |||
7c6de7e19c | |||
80dbfc0264 | |||
8a23f451b3 | |||
9097874854 | |||
8509f17889 | |||
5b2a524301 | |||
8a6c11b6b5 | |||
a79dfc575e | |||
3b8c412885 | |||
428b9de85c | |||
5a3668d6a9 | |||
8970e89cc0 | |||
387688caca | |||
a9201a7060 | |||
760c33b225 | |||
2e96e31a5b | |||
6d1d5ce1c6 | |||
648ff6c016 | |||
1d92534ffd | |||
81c1f94daf | |||
4d0148e2a3 | |||
fef86eef7a | |||
871a000cbd | |||
c39c45a23a | |||
cb84445159 | |||
1093bc15e5 | |||
9e2e3bc35b | |||
caec86c5a0 | |||
83beaa3ad5 | |||
ee02a7647b | |||
bcbd2a9077 | |||
0b8066923b | |||
06020740cc | |||
903c870dba | |||
4a17512819 | |||
16193b5554 | |||
e09c38ee29 | |||
7ed2df2399 | |||
23cbe26d94 | |||
13aebae71f | |||
93317350f1 | |||
e67fcca02c | |||
735665bb92 | |||
0ffa57373a | |||
6d8520a016 | |||
ff898c84c8 | |||
dc88d5a965 | |||
cdc008e361 | |||
7549acb20c | |||
aee1a1bf24 | |||
4ebcbe36e3 | |||
acc231027b | |||
6dcaf3fbab | |||
e1d26fa2e1 | |||
29f1672020 | |||
87a41be6de | |||
e91035c43a | |||
66ca05f744 | |||
9e59e7a923 | |||
afdf8430d0 | |||
b33acff21e | |||
f463b957bd | |||
e254463b0e | |||
9291137b94 | |||
ad32fcdb0f | |||
f025ecd0bb | |||
5ebcc4feb6 | |||
3a95c1e162 | |||
c9759a0fc5 | |||
9558a1d593 | |||
7cd8d0ffa8 | |||
e4fe586db3 | |||
6357eddbec | |||
7a22f73d36 | |||
a32507a7fb | |||
12bde0fa39 | |||
5d8b913d78 | |||
8824959715 | |||
c7f5082671 | |||
cf19b724ea | |||
6e2df59c93 | |||
f85e766a5a | |||
af5356f07b | |||
54b10ad726 | |||
5586619abf | |||
801108c70c | |||
7ea35e539e | |||
7e9ab45492 | |||
3961234b12 | |||
b5682bf435 | |||
1fd35de475 | |||
19c65d7ce1 | |||
e5134fd0a0 | |||
558f8330ca | |||
08adcd2f98 | |||
aaf3c56565 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
fzf-vcal
|
||||
fzf-vcal.debug
|
||||
|
7
LICENSE
Normal file
7
LICENSE
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright © 2025 Ämin Baumeler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
154
README.md
Normal file
154
README.md
Normal file
@@ -0,0 +1,154 @@
|
||||
A [fzf](https://github.com/junegunn/fzf)-based **calendar** application with CalDav support.
|
||||
If you are interested in this, then you may also be interested in the
|
||||
corresponding journaling application
|
||||
[fzf-vjour](https://github.com/baumea/fzf-vjour).
|
||||
|
||||
Description and Use Case
|
||||
------------------------
|
||||
This application allows for a keyboard-controlled maneuvering of your calendar entries.
|
||||
These entries are stored as [iCalendar](https://datatracker.ietf.org/doc/html/rfc5545) files of the type `VEVENT`.
|
||||
|
||||
For instance, you could use this application in a setup with a CalDav server,
|
||||
such as [Radicale](https://radicale.org/), and a synchronization tool like
|
||||
[vdirsyncer](http://vdirsyncer.pimutils.org/).
|
||||
|
||||
Installation
|
||||
------------
|
||||
Download the file `fzf-vcal` from the [latest release](https://github.com/baumea/fzf-vcal/releases/latest), or run `./scripts/build.sh`, then
|
||||
copy `fzf-vcal` to your preferred location, e.g., `~/.local/bin`, and make it executable.
|
||||
|
||||
### Requirements
|
||||
This is a POSIX script with inline `awk` elements.
|
||||
Make sure you have [fzf](https://github.com/junegunn/fzf) installed.
|
||||
I also suggest to install [batcat](https://github.com/sharkdp/bat) for colorful previews.
|
||||
|
||||
Configuration
|
||||
--------------
|
||||
This application is configured with a file located at `$HOME/.config/fzf-vcal/config`.
|
||||
The entry `ROOT` specifies the root directory of your calendar entries.
|
||||
This directory may contain several subfolders, called _collections_.
|
||||
The entry `COLLECTION_LABELS` is a `;`-delimited list, where each item specifies a subfolder and a label (see example below).
|
||||
In the application, the user sees the collection labels instead of the collection names.
|
||||
This is particularly useful, because some servers use randomly generated names.
|
||||
Finally, a third entry `SYNC_CMD` specifies the command to be executed for synchronizing.
|
||||
|
||||
Consider the following example:
|
||||
```sh
|
||||
ROOT=~/.calendar/
|
||||
COLLECTION_LABELS="745ae7a0-d723-4cd8-80c4-75f52f5b7d90=👫🏼;12cacb18-d3e1-4ad4-a1d0-e5b209012e85=💼;"
|
||||
SYNC_CMD="vdirsyncer sync calendar"
|
||||
```
|
||||
|
||||
|
||||
Here, the files are stored in
|
||||
`~/.journal/12cacb18-d3e1-4ad4-a1d0-e5b209012e85` (work-related entries)
|
||||
and
|
||||
`~/.journal/745ae7a0-d723-4cd8-80c4-75f52f5b7d90` (shared collection).
|
||||
|
||||
This configuration will work well with a `vdirsyncer` configuration such as
|
||||
```confini
|
||||
[pair calendar]
|
||||
a = "local"
|
||||
b = "remote"
|
||||
collections = ["from a", "from b"]
|
||||
|
||||
[storage local]
|
||||
type = "filesystem"
|
||||
fileext = ".ics"
|
||||
path = "~/.calendar"
|
||||
|
||||
[storage remote]
|
||||
type = "caldav"
|
||||
item_types = ["VEVENT"]
|
||||
...
|
||||
```
|
||||
|
||||
Here is the complete list of configuration options:
|
||||
|
||||
```
|
||||
### ROOT: Directory containing the collections
|
||||
### COLLECTION_LABELS: Mappings between collections and labels
|
||||
### SYNC_CMD (optional): Synchronization command
|
||||
### DAY_START (optional): Hour of start of the day (defaults to 8)
|
||||
### DAY_END (optional): Hour of end of the day (defaults to 18)
|
||||
### EDITOR (optional): Your favorite editor, is usually already exported
|
||||
### TZ (optional): Your favorite timezone, usually system's choice
|
||||
### LC_TIME (optional): Your favorite locale for date and time
|
||||
### ZI_DIR (optional): Location of tzdata, defaults to /usr/share/zoneinfo
|
||||
```
|
||||
|
||||
Usage
|
||||
-----
|
||||
After starting `fzf-vcal`, you are presented with a view on the current week.
|
||||
You can navigate that week using `j` and `h` for going down and up.
|
||||
Hit `<enter>` on any day, and you will see all entries for that date, including
|
||||
previews. In both, the week and day views, you can add entries by hitting
|
||||
`ctrl-n`.
|
||||
|
||||
Here is the list of all available keybindings:
|
||||
|
||||
### Week view
|
||||
|
||||
| Key | Action |
|
||||
| --- | ------ |
|
||||
| `q` | quit |
|
||||
| `enter` | open day |
|
||||
| `j` | down |
|
||||
| `k` | up |
|
||||
| `l` | go to next week |
|
||||
| `h` | go to previous week |
|
||||
| `ctrl-l` | go to next month |
|
||||
| `ctrl-h` | go to previous month |
|
||||
| `alt-l` | go to next year |
|
||||
| `alt-h` | go to previous year |
|
||||
| `ctrl-r` | reload and go to week that contains `today` |
|
||||
| `ctrl-g` | interactively go to specified week |
|
||||
| `ctrl-t` | set timezon |
|
||||
| `ctrl-s` | synchronize |
|
||||
| `ctrl-n` | add new entry |
|
||||
| `\` | search all appointment s |
|
||||
| `x` | Cancel and confirm entry |
|
||||
| `c` | Unconfirm and confirm entry |
|
||||
|
||||
### Day view
|
||||
|
||||
| Key | Action |
|
||||
| --- | ------ |
|
||||
| `enter` | edit appointment |
|
||||
| `a` | open attachment list of appointment |
|
||||
| `j` | down |
|
||||
| `k` | up |
|
||||
| `l` | go to next day |
|
||||
| `h` | go to previous day |
|
||||
| `ctrl-l` | go to next week |
|
||||
| `ctrl-h` | go to previous week |
|
||||
| `alt-l` | go to next month |
|
||||
| `alt-h` | go to previous month |
|
||||
| `ctrl-r` | reload and go to `today` |
|
||||
| `ctrl-g` | interactively go to specified day |
|
||||
| `ctrl-t` | set timezon |
|
||||
| `ctrl-s` | synchronize |
|
||||
| `ctrl-n` | add new entry |
|
||||
| `ctrl-alt-d` | delete entry |
|
||||
| `w` | toggle line wrap in preview |
|
||||
| `ctrl-d` | down in preview |
|
||||
| `ctrl-u` | up in preview |
|
||||
| `alt-v` | view raw iCalendar file |
|
||||
| `esc` | return to week view, you can also do this with `q` or `backspace` |
|
||||
|
||||
|
||||
### There is more
|
||||
|
||||
You may also invoke the script with `--help` to see further command-line options.
|
||||
|
||||
Also, you may set `LC_TIME` to your preferred language, and `TZ` to your
|
||||
preferred timezone. The latter is in particular helpful if you want to take a
|
||||
look at your calendar relative to being in another timezone.
|
||||
|
||||
Git support
|
||||
-----------
|
||||
You can track your events with `git` by simply running `fzf-vcal --git-init`.
|
||||
|
||||
License
|
||||
-------
|
||||
This project is licensed under the [MIT License](./LICENSE).
|
@@ -5,7 +5,15 @@ GREEN="\033[0;32m"
|
||||
OFF="\033[m"
|
||||
NAME="fzf-vcal"
|
||||
SRC="./src/main.sh"
|
||||
echo "🐔 ${GREEN}Building${OFF} ${BOLD}$NAME${OFF}"
|
||||
sed -E 's|@@include (.+)$|cat \1|e' "$SRC" >"$NAME"
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
echo "🐔 ${GREEN}Internalize sourced files${OFF}"
|
||||
sed -E 's|\. "([^$].+)"$|cat src/\1|e' "$SRC" >"$tmpdir/1.sh"
|
||||
echo "🥚 ${GREEN}Internalize awk scripts${OFF}"
|
||||
sed -E 's|@@include (.+)$|cat src/\1|e' "$tmpdir/1.sh" >"$tmpdir/2.sh"
|
||||
echo "🐔 ${GREEN}Internalize awk libraries${OFF}"
|
||||
sed -E 's|@include "(.+)"$|cat src/\1|e' "$tmpdir/2.sh" >"$NAME"
|
||||
echo "🥚 ${GREEN}Make executable and cleanup${OFF}"
|
||||
chmod +x "$NAME"
|
||||
echo "🥚 ${GREEN}Done${OFF}"
|
||||
rm -rf "$tmpdir"
|
||||
echo "🍳 ${GREEN}Done:${OFF} Sucessfully built ${BOLD}${GREEN}$NAME${OFF}"
|
||||
|
66
src/awk/approx.awk
Normal file
66
src/awk/approx.awk
Normal file
@@ -0,0 +1,66 @@
|
||||
## src/awk/approx.awk
|
||||
##
|
||||
## Generate single-line approximate information for every iCalendar argument.
|
||||
## The fields in each line are separated by "\t"
|
||||
## The fields are the following:
|
||||
## 1. "~" (constant, indicating that the lines contains approximate information)
|
||||
## 2. start (this can be used in date (1))
|
||||
## 3. end (this can be used in date (1)
|
||||
## 4. string to display
|
||||
## 5. filename (collection/name)
|
||||
##
|
||||
## @assign collection_labels: See configuration of the current program.
|
||||
## @assign style_line: Style for each line
|
||||
|
||||
@include "lib/awk/icalendar.awk"
|
||||
|
||||
# Functions
|
||||
|
||||
# Get relative file path.
|
||||
#
|
||||
# @local variables: n, a
|
||||
# @input path: Path to file
|
||||
# @return: File path of depth 1
|
||||
function fn(path, n, a) {
|
||||
n = split(path, a, "/")
|
||||
return a[n-1] "/" a[n]
|
||||
}
|
||||
|
||||
# Generate title string that will be displayed to user. Here, the start date
|
||||
# gets a monthly resolution.
|
||||
#
|
||||
# @input start: Parsed content of DTSTART field
|
||||
# @input summary: Content of SUMMARY field
|
||||
# @return: colorized single-line title string
|
||||
function title(start, summary) {
|
||||
summary = getcontent(summary)
|
||||
gsub("\n", " ", summary) # This will be put on a single line
|
||||
gsub("\t", " ", summary) # we use "\t" as delimiter
|
||||
depth = split(FILENAME, path, "/")
|
||||
collection = depth > 1 ? path[depth-1] : ""
|
||||
collection = collection in collection2label ? collection2label[collection] : collection
|
||||
return style_line "~ " collection " " gensub(/^[^0-9]*([0-9]{4})([0-9]{2}).*$/, "\\1-\\2", "1", start) " " summary OFF
|
||||
}
|
||||
|
||||
# AWK program
|
||||
BEGIN {
|
||||
FS="[:;=]"
|
||||
OFS="\t"
|
||||
split(collection_labels, mapping, ";")
|
||||
for (map in mapping)
|
||||
{
|
||||
split(mapping[map], m, "=")
|
||||
collection2label[m[1]] = m[2]
|
||||
}
|
||||
# Colors
|
||||
OFF = "\033[m"
|
||||
}
|
||||
BEGINFILE { inside = 0; rs = 0; dur = 0; summary = ""; start = "ERROR"; end = "ERROR" }
|
||||
/^END:VEVENT/ { print "~", start, dur ? start " " end : end, title(start, summary), fn(FILENAME); nextfile }
|
||||
/^DTSTART/ && inside { start = parse_dt(getparam($0), getcontent($0)) }
|
||||
/^DTEND/ && inside { end = parse_dt(getparam($0), getcontent($0)) }
|
||||
/^DURATION/ && inside { end = parse_duration($NF); dur = 1 }
|
||||
/^[^ ]/ && rs { rs = 0 }
|
||||
/^ / && rs { summary = summary substr($0, 2) }
|
||||
/^SUMMARY/ && inside { rs = 1; summary = $0 }
|
||||
/^BEGIN:VEVENT/ { inside = 1 }
|
35
src/awk/attach.awk
Normal file
35
src/awk/attach.awk
Normal file
@@ -0,0 +1,35 @@
|
||||
## src/awk/attach.awk
|
||||
## Prepend attachment to iCalendar file.
|
||||
##
|
||||
## @assign file: Path to base64-encoded content
|
||||
## @assign mime: Mime
|
||||
## @assign filename: Original filename
|
||||
|
||||
# Functions
|
||||
|
||||
# Write attachment
|
||||
#
|
||||
# @local variables: line, aline
|
||||
function write_attachment( line, aline, fl) {
|
||||
line = "ATTACH;ENCODING=BASE64;VALUE=BINARY;FMTTYPE="mime";FILENAME="filename":"
|
||||
fl = 1
|
||||
while (getline aline <file) {
|
||||
line = line aline
|
||||
if (fl && length(line) >= 73) {
|
||||
print substr(line, 1, 73)
|
||||
line = substr(line, 74)
|
||||
fl = 0
|
||||
}
|
||||
while (length(line) >= 72) {
|
||||
print " "substr(line, 1, 72)
|
||||
line = substr(line, 73)
|
||||
}
|
||||
}
|
||||
if (line)
|
||||
print " "line
|
||||
}
|
||||
|
||||
# AWK program
|
||||
|
||||
/^END:VEVENT$/ { write_attachment() }
|
||||
{ print }
|
8
src/awk/attachdd.awk
Normal file
8
src/awk/attachdd.awk
Normal file
@@ -0,0 +1,8 @@
|
||||
BEGIN { FS="[:;]" }
|
||||
/^END:VEVENT$/ { ins = 0; exit }
|
||||
/^[^ ]/ && a { a = 0 }
|
||||
/^ / && a && p { print substr($0, 2); }
|
||||
/^ / && a && !p { if (index($0, ":")) { p = 1; print substr($0, index($0, ":")+1) } }
|
||||
/^ATTACH/ && ins { i++; }
|
||||
/^ATTACH/ && ins && i == id { a = 1; if (index($0, ":")) { p = 1; print substr($0, index($0, ":")+1) } }
|
||||
/^BEGIN:VEVENT$/ { ins = 1 }
|
41
src/awk/attachls.awk
Normal file
41
src/awk/attachls.awk
Normal file
@@ -0,0 +1,41 @@
|
||||
# Decide if we need to read more to get all properties
|
||||
#
|
||||
# @input str: strin read so far
|
||||
# @return: 1 if we need more data, 0 otherwise
|
||||
function cont_reading(str) {
|
||||
return index(str, ":") ? 0 : 1
|
||||
}
|
||||
|
||||
# Get information about attachment
|
||||
#
|
||||
# @input i: Attachment index
|
||||
# @input str: Attachment string (at least up to content separator `:`)
|
||||
# @return: informative string
|
||||
function att_info(i, str, cnt, k, info) {
|
||||
str = substr(str, 1, index(str, ":") - 1)
|
||||
cnt = split(str, props)
|
||||
if (cnt > 1) {
|
||||
for (k=2; k<=cnt; k++) {
|
||||
pname = substr(props[k], 1, index(props[k], "=") - 1)
|
||||
pvalu = substr(props[k], index(props[k], "=") + 1)
|
||||
if (pname == "ENCODING" && pvalu = "BASE64")
|
||||
enc = "base64"
|
||||
if (pname == "FILENAME")
|
||||
fin = pvalu
|
||||
if (pname == "VALUE")
|
||||
val = pvalu
|
||||
if (pname == "FMTTYPE")
|
||||
type = pvalu
|
||||
}
|
||||
if (enc)
|
||||
info = "inline"
|
||||
}
|
||||
print i, fin, type, enc, info
|
||||
}
|
||||
|
||||
BEGIN { FS="[:;]"; OFS="\t" }
|
||||
/^END:VEVENT$/ { ins = 0; exit }
|
||||
l && !r { att_info(i, l); l = "" }
|
||||
/^ / && r { l = l substr($0, 2); r = cont_reading($0) }
|
||||
/^ATTACH/ && ins { i++; l = $0; r = cont_reading($0) }
|
||||
/^BEGIN:VEVENT$/ { ins = 1 }
|
13
src/awk/attachrm.awk
Normal file
13
src/awk/attachrm.awk
Normal file
@@ -0,0 +1,13 @@
|
||||
## src/awk/attachrm.awk
|
||||
## Remove attachment from iCalendar file.
|
||||
##
|
||||
## @assign id: Attachment number to remove
|
||||
|
||||
BEGIN { FS="[:;]" }
|
||||
/^END:VEVENT$/ { ins = 0 }
|
||||
/^[^ ]/ && a { a = 0 }
|
||||
/^ / && a { next }
|
||||
/^ATTACH/ && ins { i++; }
|
||||
/^ATTACH/ && ins && i == id { a = 1; next }
|
||||
/^BEGIN:VEVENT$/ { ins = 1 }
|
||||
{ print }
|
26
src/awk/calannot.awk
Normal file
26
src/awk/calannot.awk
Normal file
@@ -0,0 +1,26 @@
|
||||
## src/awk/calannot.awk
|
||||
## Annotate monthly calendar
|
||||
##
|
||||
## @assign cur: Day-of-month to mark as `today`
|
||||
## @assign day: Day-of-month to highlight
|
||||
## @assign style_month: Theme to use for month
|
||||
## @assign style_weekdays: Theme to use for weekdays
|
||||
## @assign style_cur: Theme to use for current day
|
||||
## @assign style_highlight: Theme to use for highlighted day
|
||||
|
||||
BEGIN {
|
||||
OFF = "\033[m"
|
||||
day = day + 0
|
||||
cur = cur + 0
|
||||
}
|
||||
NR == 1 { print style_month $0 OFF; next }
|
||||
NR == 2 { print style_weekdays $0 OFF; next }
|
||||
{
|
||||
if (day == cur) {
|
||||
sub("\\y"cur"\\y", style_highlight style_cur cur OFF)
|
||||
} else {
|
||||
sub("\\y"cur"\\y", style_cur cur OFF)
|
||||
sub("\\y"day"\\y", style_highlight day OFF)
|
||||
}
|
||||
print
|
||||
}
|
23
src/awk/calshift.awk
Normal file
23
src/awk/calshift.awk
Normal file
@@ -0,0 +1,23 @@
|
||||
## src/awk/calshift.awk
|
||||
## Rearrange days of a monthly output from cal (1), such that Monday is the
|
||||
## first day of the week.
|
||||
|
||||
BEGIN {
|
||||
ORS = ""
|
||||
W3 = " "
|
||||
W17 = W3 W3 W3 W3 W3 " "
|
||||
}
|
||||
NR == 1 { i++; print $0 "\n"; next }
|
||||
NR == 2 { i++; print substr($0, 4, 17) " " substr($0, 1, 3) " \n"; next }
|
||||
NR == 3 && /^ 1/ { print W17; }
|
||||
NR == 3 && /^ / { print substr($0, 4, 17); next }
|
||||
/[0-9]/ {
|
||||
i++
|
||||
print " " substr($0, 1, 3) " \n" substr($0, 4, 17)
|
||||
}
|
||||
END {
|
||||
i++
|
||||
print " " W3 " \n"
|
||||
for (i; i<8; i++)
|
||||
print " " W17 W3 " \n"
|
||||
}
|
125
src/awk/dayview.awk
Normal file
125
src/awk/dayview.awk
Normal file
@@ -0,0 +1,125 @@
|
||||
## src/awk/dayview.awk
|
||||
## Take as input (tab-delimited):
|
||||
## 1. s (start time, as HH:MM)
|
||||
## 2. e (end time, as HH:MM)
|
||||
## 3. starttime
|
||||
## 4. endtime
|
||||
## 5. fpath
|
||||
## 6. collection
|
||||
## 7. description
|
||||
## 8. status
|
||||
##
|
||||
## filter out irrelevant lines, and generate the view of a day
|
||||
## (tab-delimited), including empty hours:
|
||||
## 1. start date
|
||||
## 2. start time
|
||||
## 3. end time
|
||||
## 4. file path
|
||||
## 5. collection
|
||||
## 6. description
|
||||
##
|
||||
## @assign today: Date of `today` in the format %D (%m/%d/%y)
|
||||
## @assign daystart: Hour of start of the day
|
||||
## @assign dayend: Hour of end of the day
|
||||
## @assign style_allday
|
||||
## @assign style_timerange
|
||||
## @assign style_confirmed
|
||||
## @assign style_tentative
|
||||
## @assign style_cancelled
|
||||
## @assign style_hour
|
||||
## @assign style_emptyhour
|
||||
|
||||
# Functions
|
||||
|
||||
# Set event color based on status
|
||||
|
||||
# @input status: Event status, one of TENTATIVE, CONFIRMED, CANCELLED
|
||||
# @return: Color modifier
|
||||
function color_from_status(status) {
|
||||
return status == "CANCELLED" ? style_cancelled : status == "TENTATIVE" ? style_tentative : style_confirmed
|
||||
}
|
||||
|
||||
# Return line for all-day event.
|
||||
#
|
||||
# @local variables: color
|
||||
# @input collection: Collection symbol
|
||||
# @input desc: Event description
|
||||
# @input status: Event status, one of TENTATIVE, CONFIRMED, CANCELLED
|
||||
# @return: Single-line string
|
||||
function allday(collection, desc, status, color) {
|
||||
color = color_from_status(status)
|
||||
return collection " " style_allday color desc OFF
|
||||
}
|
||||
|
||||
# Return line for multi-day event, or event that starts at midnight, which ends today.
|
||||
#
|
||||
# @local variables: color
|
||||
# @input stop: Time at which the event ends
|
||||
# @input collection: Collection symbol
|
||||
# @input desc: Event description
|
||||
# @input status: Event status, one of TENTATIVE, CONFIRMED, CANCELLED
|
||||
# @return: Single-line string
|
||||
function endstoday(stop, collection, desc, status) {
|
||||
color = color_from_status(status)
|
||||
return collection " " style_timerange " → " stop ": " OFF color desc OFF
|
||||
}
|
||||
|
||||
# Return line for event that starts sometime today.
|
||||
#
|
||||
# @local variables: color
|
||||
# @input start: Time at which the event starts
|
||||
# @input stop: Time at which the event ends
|
||||
# @input collection: Collection symbol
|
||||
# @input desc: Event description
|
||||
# @input status: Event status, one of TENTATIVE, CONFIRMED, CANCELLED
|
||||
# @return: Single-line string
|
||||
function slice(start, stop, collection, desc, status) {
|
||||
color = color_from_status(status)
|
||||
if (stop == "00:00")
|
||||
return collection " " style_timerange start " → " ": " OFF color desc OFF
|
||||
else
|
||||
return collection " " style_timerange start " – " stop ": " OFF color desc OFF
|
||||
}
|
||||
|
||||
# Print line for a single hour entry.
|
||||
#
|
||||
# @input hour: Hour of the entry
|
||||
function hrline(hour) {
|
||||
hour = hour < 10 ? "0"hour : hour
|
||||
print today, hour, "", "", "", " " style_hou hour ":00" OFF " " style_emptyhour
|
||||
}
|
||||
|
||||
# Print lines for hour entries before an event that starts at `start` and stops
|
||||
# at `stop`.
|
||||
#
|
||||
# @local variables: starth, stoph, tmp, i
|
||||
# @input start: Time at which the event starts
|
||||
# @input stop: Time at which the event ends
|
||||
# @input h: Last event-free hour
|
||||
# @return: Hour of now last event-free hour
|
||||
function hrlines(start, stop, h, starth, stoph, tmp, i) {
|
||||
starth = substr(start, 1, 2)
|
||||
stoph = substr(stop, 1, 2)
|
||||
tmp = substr(start, 4, 2) == "00" ? 0 : 1
|
||||
for (i=h; i < starth + tmp && i < dayend; i++)
|
||||
hrline(i)
|
||||
tmp = substr(stop, 4, 2) == "00" ? 0 : 1
|
||||
if (stoph + tmp < daystart)
|
||||
return daystart
|
||||
else
|
||||
return stoph + tmp
|
||||
}
|
||||
|
||||
# AWK program
|
||||
BEGIN {
|
||||
FS = "\t"
|
||||
OFS = "\t"
|
||||
OFF = "\033[m"
|
||||
}
|
||||
$1 == "00:00" && $2 == "00:00" { print today, $1, $3, $4, $5, allday($6, $7, $8); next }
|
||||
$1 == "00:00" { print today, $1, $3, $4, $5, endstoday($2, $6, $7, $8); next }
|
||||
$1 ~ /^[0-9]{2}:[0-9]{2}$/ {
|
||||
daystart = hrlines($1, $2, daystart)
|
||||
print today, $1, $3, $4, $5, slice($1, $2, $6, $7, $8)
|
||||
}
|
||||
END { hrlines(dayend":00", 0, daystart) }
|
18
src/awk/get.awk
Normal file
18
src/awk/get.awk
Normal file
@@ -0,0 +1,18 @@
|
||||
## src/awk/get.awk
|
||||
## Print content of a field of an iCalendar file.
|
||||
##
|
||||
## @assign field: Field name
|
||||
|
||||
@include "lib/awk/icalendar.awk"
|
||||
|
||||
BEGIN { FS = ":"; regex = "^" field }
|
||||
/^BEGIN:VEVENT$/ { inside = 1 }
|
||||
/^END:VEVENT$/ { exit }
|
||||
$0 ~ regex { content = $0; next }
|
||||
/^ / && content { content = content substr($0, 2); next }
|
||||
/^[^ ]/ && content { exit }
|
||||
END {
|
||||
if (!inside) { exit }
|
||||
# Process content line
|
||||
print getcontent(content)
|
||||
}
|
10
src/awk/has.awk
Normal file
10
src/awk/has.awk
Normal file
@@ -0,0 +1,10 @@
|
||||
## src/awk/has.awk
|
||||
## Decide if VEVENT file has a specific field.
|
||||
##
|
||||
## @assign field: Field name
|
||||
|
||||
# AWK program
|
||||
BEGIN { FS = "[:;]" }
|
||||
/^BEGIN:VEVENT$/ { ins = 1 }
|
||||
/^END:VEVENT$/ { exit 1 }
|
||||
ins && $1 == field { exit 0 }
|
@@ -1,79 +0,0 @@
|
||||
function parse( dt) {
|
||||
# Get timezone information
|
||||
dt = "";
|
||||
for (i=2; i<NF-1; i+=2) {
|
||||
if ($i == "TZID") {
|
||||
dt = "TZ=\"" $(i+1) "\" ";
|
||||
break;
|
||||
}
|
||||
}
|
||||
# Get date/datetime
|
||||
return length($NF) == 8 ?
|
||||
dt $NF :
|
||||
dt gensub(/^([0-9]{8})T([0-9]{2})([0-9]{2})([0-9]{2})(Z)?$/, "\\1 \\2:\\3:\\4\\5", "g", $NF);
|
||||
}
|
||||
|
||||
function parse_duration( dt, dta, i, n, a, seps) {
|
||||
n = split($NF, a, /[PTWHMSD]/, seps);
|
||||
delete dta;
|
||||
for (i=2; i<=n; i++) {
|
||||
if(seps[i] == "W") dta["weeks"] = a[i];
|
||||
if(seps[i] == "H") dta["hours"] = a[i];
|
||||
if(seps[i] == "M") dta["minutes"] = a[i];
|
||||
if(seps[i] == "S") dta["seconds"] = a[i];
|
||||
if(seps[i] == "D") dta["days"] = a[i];
|
||||
}
|
||||
dt = a[1] ? a[1] : "+";
|
||||
for (i in dta) {
|
||||
dt = dt " " dta[i] " " i;
|
||||
}
|
||||
return dt;
|
||||
}
|
||||
|
||||
function fn(path, n, a) {
|
||||
n = split(path, a, "/");
|
||||
return a[n-1] "/" a[n];
|
||||
}
|
||||
|
||||
function title(start, summary) {
|
||||
summary = substr(summary, index(summary, ":") + 1);
|
||||
#gsub("\\\\n", "\n", summary); # one-liner
|
||||
#gsub("\\\\N", "\n", summary); # one-liner
|
||||
gsub("\\\\n", " ", summary);
|
||||
gsub("\\\\N", " ", summary);
|
||||
gsub("\\\\,", ",", summary);
|
||||
gsub("\\\\;", ";", summary);
|
||||
gsub("\\\\\\\\", "\\", summary);
|
||||
gsub("\\|", ":", summary); # we use "|" as delimiter
|
||||
depth = split(FILENAME, path, "/");
|
||||
collection = depth > 1 ? path[depth-1] : "";
|
||||
collection = collection in collection2label ? collection2label[collection] : collection;
|
||||
return FAINT "~ " collection " " gensub(/^[^0-9]*([0-9]{4})([0-9]{2}).*$/, "\\1-\\2", "1", start) " " summary " ;" start OFF
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
FS="[:;=]";
|
||||
OFS="|"
|
||||
split(collection_labels, mapping, ";");
|
||||
for (map in mapping)
|
||||
{
|
||||
split(mapping[map], m, "=");
|
||||
collection2label[m[1]] = m[2];
|
||||
}
|
||||
# Colors
|
||||
GREEN = "\033[1;32m";
|
||||
RED = "\033[1;31m";
|
||||
WHITE = "\033[1;97m";
|
||||
CYAN = "\033[1;36m";
|
||||
FAINT = "\033[2m";
|
||||
OFF = "\033[m";
|
||||
}
|
||||
BEGINFILE { inside = 0; rs = 0; dur = 0; summary = ""; start = "ERROR"; end = "ERROR" }
|
||||
/^END:VEVENT/ { print start, dur ? start " " end : end, title(start, summary), fn(FILENAME, n, a); nextfile }
|
||||
/^DTSTART/ && inside { start = parse( dt) }
|
||||
/^DTEND/ && inside { end = parse( dt) }
|
||||
/^DURATION/ && inside { end = parse_duration( dt, dta, i, n, a, seps); dur = 1 }
|
||||
/^[^ ]/ && rs { rs = 0 }
|
||||
/^ / && rs { summary = summary substr($0, 2); }
|
||||
/^SUMMARY/ && inside { rs = 1; summary = $0; }
|
||||
/^BEGIN:VEVENT/ { inside = 1 }
|
@@ -1,17 +1,35 @@
|
||||
BEGIN { FS="|"; i=0; dlt = -259200; spw = 604800; }
|
||||
## src/awk/merge.awk
|
||||
## Merge a file that contains pairs of lines for start and end dates of events
|
||||
## with the approximate data file, and group the iCalendar file paths according
|
||||
## to the weeks at which the events take place.
|
||||
|
||||
# AWK program
|
||||
BEGIN { FS="\t"; OFS="\t" }
|
||||
NR == FNR {
|
||||
i = i + 1;
|
||||
from[i] = int(($1 + dlt)/ spw);
|
||||
getline;
|
||||
to[i] = int(($1 + dlt) / spw);
|
||||
i = i + 1
|
||||
split($0, parts, ":")
|
||||
from_year[i] = parts[1]
|
||||
from_week[i] = parts[2]
|
||||
getline
|
||||
split($0, parts, ":")
|
||||
to_year[i] = parts[1]
|
||||
to_week[i] = parts[2]
|
||||
next
|
||||
} # Load start and end week numbers from first file
|
||||
|
||||
{
|
||||
if (from[FNR] > to[FNR])
|
||||
print "FNR", FNR, ":", from[FNR],"-",to[FNR], " ",$0;
|
||||
for(i=from[FNR]; i<=to[FNR]; i++) {
|
||||
week[i] = week[i] ? week[i] ";" $4 : $4
|
||||
year_i = from_year[FNR]
|
||||
week_i = from_week[FNR]
|
||||
year_end = to_year[FNR]
|
||||
week_end = to_week[FNR]
|
||||
while(year_i <= year_end && (year_i < year_end || week_i <= week_end)) {
|
||||
label = year_i ":" week_i ":"
|
||||
week[label] = week[label] ? week[label] " " $5 : $5
|
||||
week_i++
|
||||
if (week_i > 53) {
|
||||
week_i = 1
|
||||
year_i++
|
||||
}
|
||||
}
|
||||
}
|
||||
END { for (i in week) print i, week[i]; }
|
||||
END { for (label in week) print label, week[label] }
|
||||
|
105
src/awk/new.awk
Normal file
105
src/awk/new.awk
Normal file
@@ -0,0 +1,105 @@
|
||||
## src/awk/new.awk
|
||||
## Generate iCalendar file from markdown description.
|
||||
##
|
||||
## @assign uid: UID to use
|
||||
|
||||
@include "lib/awk/icalendar.awk"
|
||||
|
||||
# AWK program
|
||||
BEGIN {
|
||||
FS=":"
|
||||
zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1)
|
||||
}
|
||||
readdesc { desc = desc ? desc "\\n" escape($0) : escape($0); next }
|
||||
{
|
||||
from = substr($0, 1, 6) == "::: |>" ? substr($0, 8) : ""
|
||||
if (!from)
|
||||
exit 1
|
||||
getline
|
||||
to = substr($0, 1, 6) == "::: <|" ? substr($0, 8) : ""
|
||||
if (!to)
|
||||
exit 1
|
||||
getline
|
||||
location = substr($0, 1, 2) == "@ " ? escape(substr($0, 3)) : ""
|
||||
if (location) getline
|
||||
summary = substr($0, 1, 2) == "# " ? escape(substr($0, 3)) : ""
|
||||
if (!summary)
|
||||
exit 1
|
||||
getline # This line should be empty
|
||||
if ($0 != "")
|
||||
exit 1
|
||||
readdesc = 1
|
||||
next
|
||||
}
|
||||
END {
|
||||
# Sanitize input
|
||||
# If nanoseconds are not 0, then we assume user entered "tomorrow" or
|
||||
# something the like, and we make this a date entry, as opposed to a
|
||||
# date-time entry.
|
||||
# Similarly, if the time is 00:00, we make this a date, as opposed to a
|
||||
# date-time entry.
|
||||
gsub("\"", "\\\"", from)
|
||||
cmd = "date -d \"" from "\" +\"%N\""
|
||||
cmd | getline n
|
||||
close(cmd)
|
||||
n = n + 0
|
||||
cmd = "date -d \"" from "\" +\"%H%M\""
|
||||
cmd | getline t
|
||||
close(cmd)
|
||||
t = t + 0
|
||||
if (n != 0 || t == 0) {
|
||||
from_type = "DATE"
|
||||
cmd = "date -d \"" from "\" +\"%Y%m%d\""
|
||||
} else {
|
||||
from_type = "DATE-TIME"
|
||||
cmd = "date -d \"" from "\" +\"@%s\" | xargs date -u +\"%Y%m%dT%H%M00Z\" -d"
|
||||
}
|
||||
suc = cmd | getline from
|
||||
close(cmd)
|
||||
if (suc != 1) {
|
||||
exit 1
|
||||
}
|
||||
#
|
||||
gsub("\"", "\\\"", to)
|
||||
cmd = "date -d \"" to "\" +\"%N\""
|
||||
cmd | getline n
|
||||
close(cmd)
|
||||
n = n + 0
|
||||
cmd = "date -d \"" to "\" +\"%H%M\""
|
||||
cmd | getline t
|
||||
close(cmd)
|
||||
t = t + 0
|
||||
if (n != 0 || t == 0) {
|
||||
to_type = "DATE"
|
||||
cmd = "date -d \"" to "\" +\"%Y%m%d\""
|
||||
} else {
|
||||
to_type = "DATE-TIME"
|
||||
cmd = "date -d \"" to "\" +\"@%s\" | xargs date -u +\"%Y%m%dT%H%M00Z\" -d"
|
||||
}
|
||||
suc = cmd | getline to
|
||||
close(cmd)
|
||||
if (suc != 1) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# print ical
|
||||
print "BEGIN:VCALENDAR"
|
||||
print "VERSION:2.0"
|
||||
print "CALSCALE:GREGORIAN"
|
||||
print "PRODID:-//fab//awk//EN"
|
||||
print "BEGIN:VEVENT"
|
||||
print "DTSTAMP:" zulu
|
||||
print "UID:" uid
|
||||
print "CLASS:PRIVATE"
|
||||
print "CREATED:" zulu
|
||||
print "SEQUENCE:1"
|
||||
print "LAST-MODIFIED:" zulu
|
||||
print "STATUS:CONFIRMED"
|
||||
print "DTSTART;VALUE=" from_type ":" from
|
||||
print "DTEND;VALUE=" to_type ":" to
|
||||
if (summary) print_fold("SUMMARY:", summary)
|
||||
if (desc) print_fold("DESCRIPTION:", desc)
|
||||
if (location) print_fold("LOCATION:", location)
|
||||
print "END:VEVENT"
|
||||
print "END:VCALENDAR"
|
||||
}
|
@@ -1,45 +1,31 @@
|
||||
function parse( dt) {
|
||||
# Get timezone information
|
||||
dt = "";
|
||||
for (i=2; i<NF-1; i+=2) {
|
||||
if ($i == "TZID") {
|
||||
dt = "TZ=\"" $(i+1) "\" ";
|
||||
break;
|
||||
}
|
||||
}
|
||||
# Get date/datetime
|
||||
return length($NF) == 8 ?
|
||||
dt $NF :
|
||||
dt gensub(/^([0-9]{8})T([0-9]{2})([0-9]{2})([0-9]{2})(Z)?$/, "\\1 \\2:\\3:\\4\\5", "g", $NF);
|
||||
}
|
||||
## src/awk/parse.awk
|
||||
## Parse iCalendar file and print its key aspects:
|
||||
## ```
|
||||
## <start> <end> <fpath> <collection> <status> <summary>
|
||||
## ```.
|
||||
## The output is space delimited.
|
||||
## Summary may contain spaces, but it's the last in the list.
|
||||
##
|
||||
## @assign collection_labels: See configuration of the current program.
|
||||
|
||||
function parse_duration( dt, dta, i, n, a, seps) {
|
||||
n = split($NF, a, /[PTWHMSD]/, seps);
|
||||
delete dta;
|
||||
for (i=2; i<=n; i++) {
|
||||
if(seps[i] == "W") dta["weeks"] = a[i];
|
||||
if(seps[i] == "H") dta["hours"] = a[i];
|
||||
if(seps[i] == "M") dta["minutes"] = a[i];
|
||||
if(seps[i] == "S") dta["seconds"] = a[i];
|
||||
if(seps[i] == "D") dta["days"] = a[i];
|
||||
}
|
||||
dt = a[1] ? a[1] : "+";
|
||||
for (i in dta) {
|
||||
dt = dt " " dta[i] " " i;
|
||||
}
|
||||
return dt;
|
||||
}
|
||||
@include "lib/awk/icalendar.awk"
|
||||
|
||||
function print_data(start, dur, end, summary, cmd, collection) {
|
||||
summary = substr(summary, index(summary, ":") + 1);
|
||||
gsub("\\\\n", " ", summary); # one-liner
|
||||
gsub("\\\\N", " ", summary); # one-liner
|
||||
gsub("\\\\,", ",", summary);
|
||||
gsub("\\\\;", ";", summary);
|
||||
gsub("\\\\\\\\", "\\", summary);
|
||||
depth = split(FILENAME, path, "/");
|
||||
collection = depth > 1 ? path[depth-1] : "";
|
||||
collection = collection in collection2label ? collection2label[collection] : collection;
|
||||
# Print string of parsed data.
|
||||
#
|
||||
# @local variables: cmd, collection, depth, path
|
||||
# @input start: Start time of event
|
||||
# @input dur: Boolean that indicates that `end` specifies a duration
|
||||
# @input end: End time of event, or event duration (see `dur`)
|
||||
# @input summary: Content of SUMMARY field of the event
|
||||
function print_data(start, dur, end, summary, cmd, collection, depth, path) {
|
||||
summary = getcontent(summary)
|
||||
gsub("\n", " ", summary) # This will be put on a single line
|
||||
gsub("\t", " ", summary) # Generally, we use tab as delimiter.
|
||||
depth = split(FILENAME, path, "/")
|
||||
fpath = path[depth-1] "/" path[depth]
|
||||
collection = depth > 1 ? path[depth-1] : ""
|
||||
collection = collection in collection2label ? collection2label[collection] : collection
|
||||
collection = collection2label[path[depth-1]]
|
||||
end = dur ? start " " end : end
|
||||
cmd = "date -d '" start "' +\"%s\""
|
||||
cmd | getline start
|
||||
@@ -47,30 +33,26 @@ function print_data(start, dur, end, summary, cmd, collection) {
|
||||
cmd = "date -d '" end "' +\"%s\""
|
||||
cmd | getline end
|
||||
close(cmd)
|
||||
print start, end, collection, summary
|
||||
status = status ? status : "CONFIRMED"
|
||||
print start, end, fpath, collection, status, summary
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
FS="[:;=]";
|
||||
split(collection_labels, mapping, ";");
|
||||
# AWK program
|
||||
BEGIN {
|
||||
FS="[:;=]"
|
||||
split(collection_labels, mapping, ";")
|
||||
for (map in mapping)
|
||||
{
|
||||
split(mapping[map], m, "=");
|
||||
collection2label[m[1]] = m[2];
|
||||
split(mapping[map], m, "=")
|
||||
collection2label[m[1]] = m[2]
|
||||
}
|
||||
# Colors
|
||||
GREEN = "\033[1;32m";
|
||||
RED = "\033[1;31m";
|
||||
WHITE = "\033[1;97m";
|
||||
CYAN = "\033[1;36m";
|
||||
FAINT = "\033[2m";
|
||||
OFF = "\033[m";
|
||||
}
|
||||
/^END:VEVENT/ && inside { print_data(start, dur, end, summary, cmd, collection); exit }
|
||||
/^DTSTART/ && inside { start = parse( dt) }
|
||||
/^DTEND/ && inside { end = parse( dt) }
|
||||
/^DURATION/ && inside { end = parse_duration( dt, dta, i, n, a, seps); dur = 1 }
|
||||
/^[^ ]/ && rs { rs = 0 }
|
||||
/^ / && rs { summary = summary substr($0, 2); }
|
||||
/^SUMMARY/ && inside { rs = 1; summary = $0; }
|
||||
/^BEGIN:VEVENT/ { inside = 1 }
|
||||
/^END:VEVENT/ && inside { print_data(start, dur, end, summary); exit }
|
||||
/^DTSTART/ && inside { start = parse_dt(getparam($0), getcontent($0)) }
|
||||
/^DTEND/ && inside { end = parse_dt(getparam($0), getcontent($0)) }
|
||||
/^DURATION/ && inside { end = parse_duration($NF); dur = 1 }
|
||||
/^STATUS/ && inside { status = $NF }
|
||||
/^[^ ]/ && rs { rs = 0 }
|
||||
/^ / && rs { summary = summary substr($0, 2) }
|
||||
/^SUMMARY/ && inside { rs = 1; summary = $0 }
|
||||
/^BEGIN:VEVENT/ { inside = 1 }
|
||||
|
27
src/awk/set.awk
Normal file
27
src/awk/set.awk
Normal file
@@ -0,0 +1,27 @@
|
||||
## src/awk/set.awk
|
||||
## Set or replace the content of a specified field in the iCalendar file.
|
||||
##
|
||||
## @assign field: iCalendar field
|
||||
## @assign value: Content to set it to
|
||||
##
|
||||
## LIMITATION: This program does not fold long content lines.
|
||||
|
||||
@include "lib/awk/icalendar.awk"
|
||||
|
||||
BEGIN { FS = "[:;]"; zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1) }
|
||||
/^BEGIN:VEVENT$/ { inside = 1 }
|
||||
/^END:VEVENT$/ {
|
||||
inside = 0
|
||||
if (!duplic)
|
||||
print field ":" escape(value)
|
||||
seq = seq ? seq + 1 : 1
|
||||
print "SEQUENCE:" seq
|
||||
print "LAST-MODIFIED:" zulu
|
||||
}
|
||||
$1 == field && inside { con = 1; duplic = 1; print field ":" escape(value); next }
|
||||
$1 == field && duplic { con = 1; next }
|
||||
/^ / && con { next }
|
||||
/^[^ ]/ && con { con = 0 }
|
||||
/^SEQUENCE/ && inside { seq = $2; next } # store sequence number and skip
|
||||
/^LAST-MODIFIED/ && inside { next }
|
||||
{ print }
|
103
src/awk/update.awk
Normal file
103
src/awk/update.awk
Normal file
@@ -0,0 +1,103 @@
|
||||
## src/awk/update.awk
|
||||
## Update iCalendar file from markdown file.
|
||||
|
||||
@include "lib/awk/icalendar.awk"
|
||||
|
||||
BEGIN {
|
||||
FS=":"
|
||||
zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1)
|
||||
}
|
||||
|
||||
ENDFILE {
|
||||
if (NR == FNR)
|
||||
{
|
||||
# If nanoseconds are not 0, then we assume user entered "tomorrow" or
|
||||
# something the like, and we make this a date entry, as opposed to a
|
||||
# date-time entry.
|
||||
gsub("\"", "\\\"", from)
|
||||
cmd = "date -d \"" from "\" +\"%N\""
|
||||
cmd | getline n
|
||||
close(cmd)
|
||||
n = n + 0
|
||||
cmd = "date -d \"" from "\" +\"%H%M\""
|
||||
cmd | getline t
|
||||
close(cmd)
|
||||
t = t + 0
|
||||
if (n != 0 || t == 0) {
|
||||
from_type = "DATE"
|
||||
cmd = "date -d \"" from "\" +\"%Y%m%d\""
|
||||
} else {
|
||||
from_type = "DATE-TIME"
|
||||
cmd = "date -d \"" from "\" +\"@%s\" | xargs date -u +\"%Y%m%dT%H%M00Z\" -d"
|
||||
}
|
||||
suc = cmd | getline from
|
||||
close(cmd)
|
||||
if (suc != 1) {
|
||||
exit 1
|
||||
}
|
||||
#
|
||||
gsub("\"", "\\\"", to)
|
||||
cmd = "date -d \"" to "\" +\"%N\""
|
||||
cmd | getline n
|
||||
close(cmd)
|
||||
n = n + 0
|
||||
cmd = "date -d \"" to "\" +\"%H%M\""
|
||||
cmd | getline t
|
||||
close(cmd)
|
||||
t = t + 0
|
||||
if (n != 0 || t == 0) {
|
||||
to_type = "DATE"
|
||||
cmd = "date -d \"" to "\" +\"%Y%m%d\""
|
||||
} else {
|
||||
to_type = "DATE-TIME"
|
||||
cmd = "date -d \"" to "\" +\"@%s\" | xargs date -u +\"%Y%m%dT%H%M00Z\" -d"
|
||||
}
|
||||
suc = cmd | getline to
|
||||
close(cmd)
|
||||
if (suc != 1) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NR == FNR && readdesc { desc = desc ? desc "\\n" escape($0) : escape($0); next }
|
||||
NR == FNR {
|
||||
from = substr($0, 1, 6) == "::: |>" ? substr($0, 8) : ""
|
||||
if (!from)
|
||||
exit 1
|
||||
getline
|
||||
to = substr($0, 1, 6) == "::: <|" ? substr($0, 8) : ""
|
||||
if (!to)
|
||||
exit 1
|
||||
getline
|
||||
location = substr($0, 1, 2) == "@ " ? escape(substr($0, 3)) : ""
|
||||
if (location) getline
|
||||
summary = substr($0, 1, 2) == "# " ? escape(substr($0, 3)) : ""
|
||||
if (!summary)
|
||||
exit 1
|
||||
getline # This line should be empty
|
||||
if ($0 != "")
|
||||
exit 1
|
||||
readdesc = 1
|
||||
next
|
||||
}
|
||||
|
||||
/^END:VEVENT$/ {
|
||||
seq = seq ? seq + 1 : 1
|
||||
print "SEQUENCE:" seq
|
||||
print "LAST-MODIFIED:" zulu
|
||||
print "DTSTART;VALUE=" from_type ":" from
|
||||
print "DTEND;VALUE=" to_type ":" to
|
||||
print_fold("SUMMARY:", summary)
|
||||
print_fold("DESCRIPTION:", desc)
|
||||
print_fold("LOCATION:", location)
|
||||
inside = ""
|
||||
skipf = 0
|
||||
}
|
||||
/^BEGIN:VEVENT$/ { inside = 1 }
|
||||
/^ / && skipf { next } # drop this folded line
|
||||
/^[^ ]/ && skipf { skipf = 0 }
|
||||
/^(DTSTART|DTEND|SUMMARY|LOCATION|CATEGORIES|DESCRIPTION|LAST-MODIFIED)/ && inside { skipf = 1; next } # skip for now, we will write updated fields at the end
|
||||
/^X-ALT-DESC/ && inside { skipf = 1; next } # skip
|
||||
/^SEQUENCE/ && inside { seq = $2; next } # store sequence number and skip
|
||||
{ print }
|
@@ -1,44 +0,0 @@
|
||||
function parse( dt) {
|
||||
# Get timezone information
|
||||
dt = "";
|
||||
for (i=2; i<NF-1; i+=2) {
|
||||
if ($i == "TZID") {
|
||||
dt = "TZ=\"" $(i+1) "\" ";
|
||||
break;
|
||||
}
|
||||
}
|
||||
# Get date/datetime
|
||||
return length($NF) == 8 ?
|
||||
dt $NF :
|
||||
dt gensub(/^([0-9]{8})T([0-9]{2})([0-9]{2})([0-9]{2})(Z)?$/, "\\1 \\2:\\3:\\4\\5", "g", $NF);
|
||||
}
|
||||
|
||||
function parse_duration( dt, dta, i, n, a, seps) {
|
||||
n = split($NF, a, /[PTWHMSD]/, seps);
|
||||
delete dta;
|
||||
for (i=2; i<=n; i++) {
|
||||
if(seps[i] == "W") dta["weeks"] = a[i];
|
||||
if(seps[i] == "H") dta["hours"] = a[i];
|
||||
if(seps[i] == "M") dta["minutes"] = a[i];
|
||||
if(seps[i] == "S") dta["seconds"] = a[i];
|
||||
if(seps[i] == "D") dta["days"] = a[i];
|
||||
}
|
||||
dt = a[1] ? a[1] : "+";
|
||||
for (i in dta) {
|
||||
dt = dt " " dta[i] " " i;
|
||||
}
|
||||
return dt;
|
||||
}
|
||||
|
||||
function fn(path, n, a) {
|
||||
n = split(path, a, "/");
|
||||
return a[n-1] "/" a[n];
|
||||
}
|
||||
|
||||
BEGIN { FS="[:;=]"; OFS="|" }
|
||||
BEGINFILE { inside = 0; dur = 0; start = "ERROR"; end = "ERROR" }
|
||||
/^END:VEVENT/ { print start, dur ? start " " end : end, fn(FILENAME, n, a); nextfile }
|
||||
/^DTSTART/ && inside { start = parse( dt) }
|
||||
/^DTEND/ && inside { end = parse( dt) }
|
||||
/^DURATION/ && inside { end = parse_duration( dt, dta, i, n, a, seps); dur = 1 }
|
||||
/^BEGIN:VEVENT/ { inside = 1 }
|
@@ -1,22 +1,39 @@
|
||||
function c() {
|
||||
return CYAN substr($0, index($0, ">") + 1) OFF " " RED "/" OFF
|
||||
## src/awk/weekview.awk
|
||||
## Print view of all appointments of the current week.
|
||||
## Generates view from
|
||||
## printf "%s\t%s\t%s\t%s\n" "$i" "$s" "$e" "$description"
|
||||
##
|
||||
## @assign startofweek: Date of first day in the week
|
||||
## @assign style_day: Style for dates
|
||||
## @assign style_event_delim: Event delimiter
|
||||
## @assign style_summary: Style for summary lines
|
||||
## @assign style_time: Style for times
|
||||
|
||||
# Functions
|
||||
|
||||
# Compose line that will display a day in the week.
|
||||
#
|
||||
# @input desc: String with a description of the event
|
||||
# @return: Single-line string
|
||||
function c(desc) {
|
||||
return style_summary desc OFF " " style_event_delim
|
||||
}
|
||||
|
||||
# AWK program
|
||||
|
||||
BEGIN {
|
||||
GREEN = "\033[1;32m";
|
||||
RED = "\033[1;31m";
|
||||
WHITE = "\033[1;97m";
|
||||
CYAN = "\033[1;36m";
|
||||
FAINT = "\033[2m";
|
||||
OFF = "\033[m";
|
||||
FS = "\t"
|
||||
OFS = "\t"
|
||||
OFF = "\033[m"
|
||||
}
|
||||
/^[0-7] 00:00 -- 00:00/ { dayline = dayline " " c(); next }
|
||||
/^[0-7] 00:00 -- / { dayline = dayline " <-|" $4 " " c(); next }
|
||||
/^[0-7] [0-9]{2}:[0-9]{2} -- 00:00/ { dayline = dayline " " $2 "|-> " c(); next }
|
||||
/^[0-7] [0-9]{2}:[0-9]{2} -- [0-9]{2}:[0-9]{2}/ { dayline = dayline " " $2 " - " $4 " " c(); next }
|
||||
/^[0-7]$/ && dayline { print dayline " ;" startofweek " +" $0 " days"; }
|
||||
/^[0-7]$/ {
|
||||
cmd = "date -d '" startofweek " +" $0 " days' +\"%a %e %b %Y\"";
|
||||
cmd | getline dayline;
|
||||
close(cmd);
|
||||
dayline = GREEN dayline ": " OFF
|
||||
$2 == "00:00" && $3 == "00:00" { dayline = dayline " " c($4); next }
|
||||
$2 == "00:00" { dayline = dayline style_time " → " $3 OFF " " c($4); next }
|
||||
$3 == "00:00" { dayline = dayline style_time " " $2 " → " OFF c($4); next }
|
||||
NF == 4 { dayline = dayline style_time " " $2 " – " $3 OFF " " c($4); next }
|
||||
NF == 1 && dayline { print "+", startofweek " +" $1-1 " days", "", dayline }
|
||||
NF == 1 {
|
||||
cmd = "date -d '" startofweek " +" $1 " days' +\"%a %e %b %Y\""
|
||||
cmd | getline dayline
|
||||
close(cmd)
|
||||
dayline = style_day dayline ": " OFF
|
||||
}
|
||||
|
121
src/lib/awk/icalendar.awk
Normal file
121
src/lib/awk/icalendar.awk
Normal file
@@ -0,0 +1,121 @@
|
||||
# Escape string to be used as content in iCalendar files.
|
||||
#
|
||||
# @input str: String to escape
|
||||
# @return: Escaped string
|
||||
function escape(str)
|
||||
{
|
||||
gsub("\\\\", "\\\\", str)
|
||||
gsub("\\n", "\\n", str)
|
||||
gsub(";", "\\;", str)
|
||||
gsub(",", "\\,", str)
|
||||
return str
|
||||
}
|
||||
|
||||
# Print property with its content and fold according to the iCalendar
|
||||
# specification.
|
||||
#
|
||||
# @local variables: i, s
|
||||
# @input nameparam: Property name with optional parameters
|
||||
# @input content: Escaped content
|
||||
function print_fold(nameparam, content, i, s)
|
||||
{
|
||||
i = 74 - length(nameparam)
|
||||
s = substr(content, 1, i)
|
||||
print nameparam s
|
||||
s = substr(content, i+1, 73)
|
||||
i = i + 73
|
||||
while (s)
|
||||
{
|
||||
print " " s
|
||||
s = substr(content, i+1, 73)
|
||||
i = i + 73
|
||||
}
|
||||
}
|
||||
|
||||
# Unescape string
|
||||
#
|
||||
# @local variables: i, c, c2, res
|
||||
# @input str: String
|
||||
# @return: Unescaped string
|
||||
function unescape(str, i, c, c2, res) {
|
||||
for(i = 1; i <= length(str); i++) {
|
||||
c = substr(str, i, 1)
|
||||
if (c != "\\") {
|
||||
res = res c
|
||||
continue
|
||||
}
|
||||
i++
|
||||
c2 = substr(str, i, 1)
|
||||
if (c2 == "n" || c2 == "N") {
|
||||
res = res "\n"
|
||||
continue
|
||||
}
|
||||
# Alternatively, c2 is "\\" or "," or ";". In each case, append res with
|
||||
# c2. If the strings has been escaped correctly, then the character c2
|
||||
# cannot be anything else. To be fail-safe, simply append res with c2.
|
||||
res = res c2
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
# Isolate parameter part of an iCalendar line.
|
||||
#
|
||||
# @input str: String
|
||||
# @return: Parameter part
|
||||
function getparam(str, i) {
|
||||
i = index(str, ";")
|
||||
if (!i)
|
||||
return ""
|
||||
return substr(str, i + 1, index(str, ":") - i)
|
||||
}
|
||||
|
||||
# Isolate content part of an iCalendar line, and unescape.
|
||||
#
|
||||
# @input str: String
|
||||
# @return: Unescaped content part
|
||||
function getcontent(str) {
|
||||
return unescape(substr(str, index(str, ":") + 1))
|
||||
}
|
||||
|
||||
# Time-zone aware parsing of DTSTART or DTEND entries.
|
||||
#
|
||||
# @local variables: tz
|
||||
# @input dt_param: iCalendar DTSTART or DTEND parameter string
|
||||
# @input dt_content: iCalendar DTSTART or DTEND content string
|
||||
# @return: date or date-time string that can be used in date (1)
|
||||
function parse_dt(dt_param, dt_content, tz, a, i, k) {
|
||||
if (dt_param) {
|
||||
split(dt_param, a, ";")
|
||||
for (i in a) {
|
||||
k = index(a[i], "=")
|
||||
if (substr(a[i], 1, k-1) == "TZID") {
|
||||
tz = "TZ=\"" substr(a[i], k + 1) "\" "
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
# Get date/date-time
|
||||
return length(dt_content) == 8 ?
|
||||
dt dt_content :
|
||||
dt gensub(/^([0-9]{8})T([0-9]{2})([0-9]{2})([0-9]{2})(Z)?$/, "\\1 \\2:\\3:\\4\\5", "g", dt_content)
|
||||
}
|
||||
|
||||
# Map iCalendar duration specification into the format to be used in date (1).
|
||||
#
|
||||
# @local variables: dt, dta, i, n, a, seps
|
||||
# @input duration: iCalendar duration string
|
||||
# @return: relative-date/date-time specification to be used in date (1)
|
||||
function parse_duration(duration, dt, dta, i, n, a, seps) {
|
||||
n = split(duration, a, /[PTWHMSD]/, seps)
|
||||
for (i=2; i<=n; i++) {
|
||||
if(seps[i] == "W") dta["weeks"] = a[i]
|
||||
if(seps[i] == "H") dta["hours"] = a[i]
|
||||
if(seps[i] == "M") dta["minutes"] = a[i]
|
||||
if(seps[i] == "S") dta["seconds"] = a[i]
|
||||
if(seps[i] == "D") dta["days"] = a[i]
|
||||
}
|
||||
dt = a[1] ? a[1] : "+"
|
||||
for (i in dta)
|
||||
dt = dt " " dta[i] " " i
|
||||
return dt
|
||||
}
|
600
src/main.sh
600
src/main.sh
@@ -1,223 +1,417 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
# TODO: Make sensitive to failures. I don't want to miss appointments!
|
||||
# TODO Ensure safe use of delimiters
|
||||
|
||||
err() {
|
||||
echo "❌ $1" >/dev/tty
|
||||
}
|
||||
if [ "${1:-}" = "--help" ]; then
|
||||
cat <<EOF
|
||||
Usage: $0 [OPTION]
|
||||
|
||||
if [ -z "${FZF_VCAL_USE_EXPORTED:-}" ]; then
|
||||
# Read configuration
|
||||
CONFIGFILE="$HOME/.config/fzf-vcal/config"
|
||||
if [ ! -f "$CONFIGFILE" ]; then
|
||||
err "Configuration '$CONFIGFILE' not found."
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck source=/dev/null
|
||||
. "$CONFIGFILE"
|
||||
if [ -z "${ROOT:-}" ] || [ -z "${SYNC_CMD:-}" ] || [ -z "${COLLECTION_LABELS:-}" ]; then
|
||||
err "Configuration is incomplete."
|
||||
exit 1
|
||||
fi
|
||||
export ROOT
|
||||
export SYNC_CMD
|
||||
export COLLECTION_LABELS
|
||||
You may specify at most one of the following options:
|
||||
--help Show this help and exit
|
||||
--today Show today's appointments
|
||||
--yesterday Show yesterday's appointments
|
||||
--tomorrow Show tomorrow's appointments
|
||||
--goto Interactively enter date to jump to
|
||||
--new [date/date-time] Create new entry (today)
|
||||
--day [date] Show appointments of specified day (today)
|
||||
--week [date] Show week of specified date (today)
|
||||
--import file Import iCalendar file
|
||||
--import-ni file Import iCalendar file non-interactively
|
||||
--git cmd Run git command cmd relative to calendar root
|
||||
--git-init Enable the use of git
|
||||
|
||||
# Tools
|
||||
if command -v "fzf" >/dev/null; then
|
||||
FZF="fzf"
|
||||
else
|
||||
err "Did not find the command-line fuzzy finder fzf."
|
||||
exit 1
|
||||
fi
|
||||
export FZF
|
||||
You may also start this program with setting locale and timezone information.
|
||||
|
||||
if command -v "uuidgen" >/dev/null; then
|
||||
UUIDGEN="uuidgen"
|
||||
else
|
||||
err "Did not find the uuidgen command."
|
||||
exit 1
|
||||
fi
|
||||
export UUIDGEN
|
||||
For instance, to see and modify all of your calendar entries from the
|
||||
perspective of Saigon, run
|
||||
TZ='Asia/Saigon' $0
|
||||
|
||||
if command -v "bat" >/dev/null; then
|
||||
CAT="bat"
|
||||
elif command -v "batcat" >/dev/null; then
|
||||
CAT="batcat"
|
||||
fi
|
||||
CAT=${CAT:+$CAT --color=always --style=numbers --language=md}
|
||||
CAT=${CAT:-cat}
|
||||
export CAT
|
||||
|
||||
### AWK SCRIPTS
|
||||
AWK_LINES=$(
|
||||
cat <<'EOF'
|
||||
@@include src/awk/lines.awk
|
||||
Likewise, you may see your calendar in the Greek language with
|
||||
LC_TIME=el_GR.UTF-8 $0
|
||||
EOF
|
||||
)
|
||||
export AWK_LINES
|
||||
|
||||
AWK_MERGE=$(
|
||||
cat <<'EOF'
|
||||
@@include src/awk/merge.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_MERGE
|
||||
|
||||
AWK_PARSE=$(
|
||||
cat <<'EOF'
|
||||
@@include src/awk/parse.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_PARSE
|
||||
|
||||
AWK_WEEKS=$(
|
||||
cat <<'EOF'
|
||||
@@include src/awk/weeks.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_WEEKS
|
||||
|
||||
AWK_WEEKVIEW=$(
|
||||
cat <<'EOF'
|
||||
@@include src/awk/weekview.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_WEEKVIEW
|
||||
### END OF AWK SCRIPTS
|
||||
FZF_VJOUR_USE_EXPORTED="yes"
|
||||
export FZF_VJOUR_USE_EXPORTED
|
||||
exit
|
||||
fi
|
||||
|
||||
__load_approx_data() {
|
||||
find "$ROOT" -type f -name '*.ics' -print0 |
|
||||
xargs -0 -P0 \
|
||||
awk \
|
||||
-v collection_labels="$COLLECTION_LABELS" \
|
||||
"$AWK_LINES"
|
||||
}
|
||||
# Configuration
|
||||
. "sh/config.sh"
|
||||
|
||||
__load_weeks() {
|
||||
dates=$(awk -F'|' '{ print $1; print $2 }' "$APPROX_DATA_FILE")
|
||||
file_dates=$(mktemp)
|
||||
echo "$dates" | date --file="/dev/stdin" +"%s" >"$file_dates"
|
||||
awk "$AWK_MERGE" "$file_dates" "$APPROX_DATA_FILE"
|
||||
rm "$file_dates"
|
||||
}
|
||||
# Theme
|
||||
. "sh/theme.sh"
|
||||
|
||||
__list() {
|
||||
weeknr=$(date -d "$DISPLAY_DATE" +"%s")
|
||||
weeknr=$(((weeknr - 259200) / 604800)) # shift, because epoch origin is a Thursday
|
||||
files=$(grep "^$weeknr " "$WEEKLY_DATA_FILE" | cut -d " " -f 2)
|
||||
dayofweek=$(date -d "$DISPLAY_DATE" +"%u")
|
||||
delta=$((1 - dayofweek))
|
||||
startofweek=$(date -d "$DISPLAY_DATE -$delta days" +"%D")
|
||||
# loop over files
|
||||
sef=$({
|
||||
IFS=';'
|
||||
set -- $files
|
||||
for file in "$@"; do
|
||||
file="$ROOT/$file"
|
||||
awk \
|
||||
-v collection_labels="$COLLECTION_LABELS" \
|
||||
"$AWK_PARSE" "$file"
|
||||
# Misc helper functions
|
||||
. "sh/misc.sh"
|
||||
|
||||
# Preview utilities
|
||||
. "sh/preview.sh"
|
||||
|
||||
# Preview command-line options
|
||||
. "sh/clipreview.sh"
|
||||
|
||||
# View utilities
|
||||
. "sh/view.sh"
|
||||
|
||||
# Reloading command-line options
|
||||
. "sh/clireload.sh"
|
||||
|
||||
# Access to awk scripts
|
||||
. "sh/awkscripts.sh"
|
||||
|
||||
# Functions to load calendar data
|
||||
. "sh/load.sh"
|
||||
|
||||
# Functions to modify iCalendar files
|
||||
. "sh/icalendar.sh"
|
||||
|
||||
# Extra, run-and-exit command-line options
|
||||
. "sh/cliextra.sh"
|
||||
|
||||
### Start
|
||||
__refresh_data
|
||||
|
||||
###
|
||||
### Main loop with the command-line argument
|
||||
### --today
|
||||
### --yesterday
|
||||
### --tomorrow
|
||||
### --goto
|
||||
### --new <optional date/date-time argument>
|
||||
### --day <optional date/date-time argument>
|
||||
### --week <optional date/date-time argument>
|
||||
### --set-tz
|
||||
###
|
||||
### The command-line argument defaults to "--week today".
|
||||
|
||||
while true; do
|
||||
export DISPLAY_DATE WEEKLY_DATA_FILE APPROX_DATA_FILE
|
||||
|
||||
case "${1:-}" in
|
||||
--today | --yesterday | --tomorrow | --goto | --new | --day | --week | --set-tz) ;;
|
||||
*)
|
||||
DISPLAY_DATE="today"
|
||||
set -- "--week" "$DISPLAY_DATE"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$1" = "--today" ]; then
|
||||
DISPLAY_DATE="today"
|
||||
set -- "--day" "$DISPLAY_DATE"
|
||||
fi
|
||||
|
||||
if [ "$1" = "--yesterday" ]; then
|
||||
DISPLAY_DATE="yesterday"
|
||||
set -- "--day" "$DISPLAY_DATE"
|
||||
fi
|
||||
|
||||
if [ "$1" = "--tomorrow" ]; then
|
||||
DISPLAY_DATE="tomorrow"
|
||||
set -- "--day" "$DISPLAY_DATE"
|
||||
fi
|
||||
|
||||
if [ "$1" = "--goto" ]; then
|
||||
DISPLAY_DATE=""
|
||||
while [ -z "${DISPLAY_DATE:-}" ]; do
|
||||
printf "Enter date you want to jump to, e.g., today + 1 month or 2024-1-14: " >/dev/tty
|
||||
read -r tmp
|
||||
if date -d "$tmp" >/dev/null; then
|
||||
DISPLAY_DATE="$(date -d "$tmp" +"%D")"
|
||||
fi
|
||||
done
|
||||
})
|
||||
if [ -n "$sef" ]; then
|
||||
sef=$(echo "$sef" | while IFS= read -r line; do
|
||||
set -- $line
|
||||
starttime="$1"
|
||||
shift
|
||||
endtime="$1"
|
||||
shift
|
||||
description="$*"
|
||||
for i in $(seq 0 7); do
|
||||
daystart=$(date -d "$startofweek +$i days 00:00:00" +"%s")
|
||||
dayend=$(date -d "$startofweek +$i days 23:59:59" +"%s")
|
||||
if [ "$starttime" -gt "$daystart" ] && [ "$starttime" -lt "$dayend" ]; then
|
||||
s=$(date -d "@$starttime" +"%H:%M")
|
||||
s="$s -"
|
||||
elif [ "$starttime" -le "$daystart" ] && [ "$endtime" -gt "$daystart" ]; then
|
||||
s="00:00 -"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
if [ "$endtime" -gt "$daystart" ] && [ "$endtime" -lt "$dayend" ]; then
|
||||
e=$(date -d "@$endtime" +"%H:%M")
|
||||
e="- $e"
|
||||
elif [ "$endtime" -ge "$dayend" ] && [ "$starttime" -lt "$dayend" ]; then
|
||||
e="- 00:00"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
echo "$i $s$e >$description"
|
||||
done
|
||||
done)
|
||||
set -- "--day" "$DISPLAY_DATE"
|
||||
fi
|
||||
sef=$({
|
||||
echo "$sef"
|
||||
seq 0 7
|
||||
} | sort -n)
|
||||
echo "$sef" | awk -v startofweek="$startofweek" "$AWK_WEEKVIEW"
|
||||
#seq -f "$startofweek +%g days" 0 6 |
|
||||
# LC_ALL=c xargs -I {} date -d "{}" +"%a %e %b %Y"
|
||||
}
|
||||
|
||||
if [ -z "${APPROX_DATA_FILE:-}" ]; then
|
||||
echo "GOING TO LOAD"
|
||||
APPROX_DATA_FILE=$(mktemp)
|
||||
__load_approx_data >"$APPROX_DATA_FILE"
|
||||
export APPROX_DATA_FILE
|
||||
fi
|
||||
if [ -z "${WEEKLY_DATA_FILE:-}" ]; then
|
||||
echo "GOING TO LOAD WD"
|
||||
WEEKLY_DATA_FILE=$(mktemp)
|
||||
__load_weeks >"$WEEKLY_DATA_FILE"
|
||||
export WEEKLY_DATA_FILE
|
||||
fi
|
||||
if [ "$1" = "--set-tz" ]; then
|
||||
new_tz=$(find "$ZI_DIR" -type f | sed "s|^$ZI_DIR/*||" | $FZF)
|
||||
if [ -n "$new_tz" ]; then
|
||||
TZ="$new_tz"
|
||||
__refresh_data
|
||||
__export
|
||||
fi
|
||||
shift
|
||||
fi
|
||||
|
||||
DISPLAY_DATE="today"
|
||||
if [ "${1:-}" = "--date" ]; then
|
||||
DISPLAY_DATE="$2"
|
||||
echo "Jumping to date $2!"
|
||||
fi
|
||||
DISPLAY_DATE=$(date -d "$DISPLAY_DATE" +"%D")
|
||||
DISPLAY_DATE_PREV=$(date -d "$DISPLAY_DATE -1 week" +"%D")
|
||||
DISPLAY_DATE_NEXT=$(date -d "$DISPLAY_DATE +1 week" +"%D")
|
||||
if [ "${1:-}" = "--new" ]; then
|
||||
__new "${2:-}"
|
||||
if [ -n "$start" ]; then
|
||||
DISPLAY_DATE="$start"
|
||||
else
|
||||
DISPLAY_DATE="${2:-}"
|
||||
fi
|
||||
__refresh_data
|
||||
__export
|
||||
set -- "--day" "$DISPLAY_DATE"
|
||||
fi
|
||||
|
||||
selection=$( (
|
||||
cut -d '|' -f 3 "$APPROX_DATA_FILE"
|
||||
yes " " | head -n 50
|
||||
__list
|
||||
) |
|
||||
$FZF \
|
||||
--tac \
|
||||
--no-sort \
|
||||
--no-hscroll \
|
||||
--ellipsis='' \
|
||||
--ansi \
|
||||
--no-clear \
|
||||
--bind="ctrl-p:become($0 --date '$DISPLAY_DATE_PREV')" \
|
||||
--bind="ctrl-n:become($0 --date '$DISPLAY_DATE_NEXT')" \
|
||||
--bind="ctrl-l:become($0)")
|
||||
if [ "$1" = "--day" ]; then
|
||||
DISPLAY_DATE="${2:-today}"
|
||||
__export
|
||||
selection=$(
|
||||
__view_day |
|
||||
$FZF \
|
||||
--reverse \
|
||||
--ansi \
|
||||
--no-sort \
|
||||
--no-input \
|
||||
--margin='20%,5%' \
|
||||
--border='double' \
|
||||
--color=label:bold:green \
|
||||
--border-label-pos=3 \
|
||||
--list-border="top" \
|
||||
--list-label-pos=3 \
|
||||
--cycle \
|
||||
--delimiter='\t' \
|
||||
--with-nth='{6}' \
|
||||
--accept-nth='1,2,3,4,5' \
|
||||
--preview="$0 --preview-event {}" \
|
||||
--expect="ctrl-n,ctrl-t,ctrl-g,ctrl-alt-d,esc,backspace,q,alt-v,x,c,a" \
|
||||
--bind='load:pos(1)+transform(
|
||||
echo change-border-label:🗓️ $(date -d {1} +"%A %e %B %Y")
|
||||
)+transform(
|
||||
[ -n "${TZ:-}" ] && echo "change-list-label:$STYLE_DV_TZ($TZ)$OFF"
|
||||
)+transform(
|
||||
[ -n {5} ] && echo show-preview
|
||||
)' \
|
||||
--bind="start:hide-preview" \
|
||||
--bind="j:down" \
|
||||
--bind="k:up" \
|
||||
--bind="l:reload:$0 --reload-day {1} '+1 day'" \
|
||||
--bind="h:reload:$0 --reload-day {1} '-1 day'" \
|
||||
--bind="right:reload:$0 --reload-day {1} '+1 day'" \
|
||||
--bind="left:reload:$0 --reload-day {1} '-1 day'" \
|
||||
--bind="ctrl-l:reload:$0 --reload-day {1} '+1 week'" \
|
||||
--bind="ctrl-h:reload:$0 --reload-day {1} '-1 week'" \
|
||||
--bind="alt-l:reload:$0 --reload-day {1} '+1 month'" \
|
||||
--bind="alt-h:reload:$0 --reload-day {1} '-1 month'" \
|
||||
--bind="ctrl-r:reload:$0 --reload-day today" \
|
||||
--bind="ctrl-s:execute($SYNC_CMD ; printf 'Press <enter> to continue.'; read -r tmp)" \
|
||||
--bind='tab:down' \
|
||||
--bind='shift-tab:up' \
|
||||
--bind='focus:hide-preview+transform(
|
||||
[ "$FZF_KEY" = "tab" ] && [ -z {5} ] && [ "$FZF_POS" -lt "$FZF_TOTAL_COUNT" ] && echo down
|
||||
[ "$FZF_KEY" = "shift-tab" ] && [ -z {5} ] && [ "$FZF_POS" -gt "1" ] && echo up
|
||||
)+transform(
|
||||
[ -n {5} ] && echo show-preview
|
||||
)' \
|
||||
--bind="w:toggle-preview-wrap" \
|
||||
--bind="ctrl-d:preview-down" \
|
||||
--bind="ctrl-u:preview-up"
|
||||
)
|
||||
key=$(echo "$selection" | head -1)
|
||||
line=$(echo "$selection" | tail -1)
|
||||
if [ "$line" = "$key" ]; then
|
||||
line=""
|
||||
fi
|
||||
DISPLAY_DATE=$(echo "$line" | cut -f 1)
|
||||
hour=$(echo "$line" | cut -f 2)
|
||||
start=$(echo "$line" | cut -f 3)
|
||||
end=$(echo "$line" | cut -f 4)
|
||||
fpath=$(echo "$line" | cut -f 5)
|
||||
if [ "$key" = "ctrl-n" ]; then
|
||||
if echo "$hour" | grep ":"; then
|
||||
hour="$DAY_START"
|
||||
fi
|
||||
set -- "--new" "$DISPLAY_DATE $hour:00"
|
||||
elif [ "$key" = "ctrl-alt-d" ] && [ -n "$fpath" ]; then
|
||||
__delete "$fpath"
|
||||
__refresh_data
|
||||
set -- "--day" "$DISPLAY_DATE"
|
||||
elif [ "$key" = "ctrl-g" ]; then
|
||||
set -- "--goto"
|
||||
elif [ "$key" = "ctrl-t" ]; then
|
||||
set -- "--set-tz" "--day" "$DISPLAY_DATE"
|
||||
elif [ "$key" = "esc" ] || [ "$key" = "backspace" ] || [ "$key" = "q" ]; then
|
||||
set -- "--week" "$DISPLAY_DATE"
|
||||
elif [ "$key" = "alt-v" ] && [ -f "$ROOT/$fpath" ]; then
|
||||
$EDITOR "$ROOT/$fpath"
|
||||
elif [ "$key" = "x" ] && [ -f "$ROOT/$fpath" ]; then
|
||||
__cancel_toggle "$fpath"
|
||||
elif [ "$key" = "c" ] && [ -f "$ROOT/$fpath" ]; then
|
||||
__tentative_toggle "$fpath"
|
||||
elif [ "$key" = "a" ] && [ -f "$ROOT/$fpath" ]; then
|
||||
att=$(
|
||||
awk "$AWK_ATTACHLS" "$ROOT/$fpath" |
|
||||
$FZF \
|
||||
--delimiter="\t" \
|
||||
--accept-nth=1,2,3,4 \
|
||||
--with-nth="Attachment {1}: \"{2}\" {3} ({5})" \
|
||||
--no-sort \
|
||||
--tac \
|
||||
--margin="30%,30%" \
|
||||
--border=bold \
|
||||
--border-label="Attachment View Keys: <enter> open, <ctrl-alt-d> delete, <shift-a> add" \
|
||||
--expect="A" \
|
||||
--expect="ctrl-c,ctrl-g,ctrl-q,ctrl-d,esc,q,backspace" \
|
||||
--print-query \
|
||||
--bind="start:hide-input" \
|
||||
--bind="ctrl-alt-d:show-input+change-query(ctrl-alt-d)+accept" \
|
||||
--bind='load:transform:[ "$FZF_TOTAL_COUNT" -eq 0 ] && echo "unbind(enter)+unbind(ctrl-alt-d)"' \
|
||||
--bind="w:toggle-wrap" \
|
||||
--bind="j:down" \
|
||||
--bind="k:up" ||
|
||||
true
|
||||
)
|
||||
key=$(echo "$att" | head -2 | xargs)
|
||||
sel=$(echo "$att" | tail -1)
|
||||
if [ "$key" = "ctrl-c" ] ||
|
||||
[ "$key" = "ctrl-g" ] ||
|
||||
[ "$key" = "ctrl-q" ] ||
|
||||
[ "$key" = "ctrl-d" ] ||
|
||||
[ "$key" = "esc" ] ||
|
||||
[ "$key" = "q" ] ||
|
||||
[ "$key" = "backspace" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ "$key" = "A" ]; then
|
||||
__add_attachment "$fpath"
|
||||
__refresh_data
|
||||
continue
|
||||
fi
|
||||
attid=$(echo "$sel" | cut -f 1)
|
||||
attname=$(echo "$sel" | cut -f 2)
|
||||
attfmt=$(echo "$sel" | cut -f 3)
|
||||
attenc=$(echo "$sel" | cut -f 4)
|
||||
if [ -z "$attid" ]; then
|
||||
# This line should be unreachable
|
||||
continue
|
||||
fi
|
||||
if [ "$key" = "ctrl-alt-d" ]; then
|
||||
while true; do
|
||||
printf "Are you sure you want to delete attachment \"%s\"? (yes/no): " "$attid" >/dev/tty
|
||||
read -r yn
|
||||
case $yn in
|
||||
"yes")
|
||||
filetmp=$(mktemp)
|
||||
awk -v id="$attid" "$AWK_ATTACHRM" "$ROOT/$fpath" >"$filetmp"
|
||||
mv "$filetmp" "$ROOT/$fpath"
|
||||
if [ -n "${GIT:-}" ]; then
|
||||
$GIT add "$fpath"
|
||||
$GIT commit -q -m "Deleted attachment from event '$(__summary_for_commit "$fpath") ...'" -- "$fpath"
|
||||
fi
|
||||
__refresh_data
|
||||
break
|
||||
;;
|
||||
"no")
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Please answer \"yes\" or \"no\"." >/dev/tty
|
||||
;;
|
||||
esac
|
||||
done
|
||||
continue
|
||||
fi
|
||||
if [ "$attenc" != "base64" ]; then
|
||||
err "Unsupported attachment encoding: $attenc"
|
||||
read -r tmp
|
||||
continue
|
||||
fi
|
||||
if [ -n "$attname" ]; then
|
||||
tmpdir=$(mktemp -d)
|
||||
attpath="$tmpdir/$attname"
|
||||
elif [ -n "$attfmt" ]; then
|
||||
attext=$(echo "$attfmt" | cut -d "/" -f 2)
|
||||
attpath=$(mktemp --suffix="$attext")
|
||||
else
|
||||
attpath=$(mktemp)
|
||||
fi
|
||||
# Get file and uncode
|
||||
awk -v id="$attid" "$AWK_ATTACHDD" "$ROOT/$fpath" | base64 -d >"$attpath"
|
||||
fn=$(file "$attpath")
|
||||
while true; do
|
||||
printf "Are you sure you want to open \"%s\"? (yes/no): " "$fn" >/dev/tty
|
||||
read -r yn
|
||||
case $yn in
|
||||
"yes")
|
||||
$OPEN "$attpath"
|
||||
printf "Press <enter> to continue." >/dev/tty
|
||||
read -r tmp
|
||||
break
|
||||
;;
|
||||
"no")
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Please answer \"yes\" or \"no\"." >/dev/tty
|
||||
;;
|
||||
esac
|
||||
done
|
||||
# Clean up
|
||||
rm -f "$attpath"
|
||||
if [ -n "${tmpdir:-}" ] && [ -d "${tmpdir:-}" ]; then
|
||||
rm -rf "$tmpdir"
|
||||
fi
|
||||
elif [ -z "$key" ] && [ -n "$fpath" ]; then
|
||||
__edit "$start" "$end" "$fpath"
|
||||
set -- "--day" "$DISPLAY_DATE"
|
||||
fi
|
||||
__export
|
||||
fi
|
||||
|
||||
if [ -z "$selection" ]; then
|
||||
rm "$WEEKLY_DATA_FILE" "$APPROX_DATA_FILE"
|
||||
return 0
|
||||
fi
|
||||
if [ "${1:-}" = "--week" ]; then
|
||||
DISPLAY_DATE="${2:-today}"
|
||||
DISPLAY_POS=$((8 - $(date -d "$DISPLAY_DATE" +"%u")))
|
||||
__export
|
||||
selection=$(
|
||||
__view_week |
|
||||
$FZF \
|
||||
--tac \
|
||||
--no-sort \
|
||||
--no-hscroll \
|
||||
--ellipsis="" \
|
||||
--delimiter="\t" \
|
||||
--with-nth="{4}" \
|
||||
--accept-nth=1,2 \
|
||||
--ansi \
|
||||
--gap 1 \
|
||||
--no-scrollbar \
|
||||
--no-input \
|
||||
--info=right \
|
||||
--margin="1" \
|
||||
--info-command="printf \"$(date +"%R %Z")\"; [ -n \"\${TZ:-}\" ] && printf \" (\$TZ)\"" \
|
||||
--preview-window=up,8,border-bottom \
|
||||
--preview="$0 --preview-week {}" \
|
||||
--bind="load:pos($DISPLAY_POS)+unbind(load)" \
|
||||
--expect="ctrl-n,ctrl-g,ctrl-t" \
|
||||
--bind="q:abort" \
|
||||
--bind="j:down" \
|
||||
--bind="k:up" \
|
||||
--bind="l:reload:$0 --reload-week {2} '+1 week'" \
|
||||
--bind="h:reload:$0 --reload-week {2} '-1 week'" \
|
||||
--bind="right:reload:$0 --reload-week {2} '+1 week'" \
|
||||
--bind="left:reload:$0 --reload-week {2} '-1 week'" \
|
||||
--bind="ctrl-l:reload:$0 --reload-week {2} '+1 month'" \
|
||||
--bind="ctrl-h:reload:$0 --reload-week {2} '-1 month'" \
|
||||
--bind="alt-l:reload:$0 --reload-week {2} '+1 year'" \
|
||||
--bind="alt-h:reload:$0 --reload-week {2} '-1 year'" \
|
||||
--bind="ctrl-r:rebind(load)+reload($0 --reload-week today)+show-preview" \
|
||||
--bind="ctrl-s:execute($SYNC_CMD ; printf 'Press <enter> to continue.'; read -r tmp)" \
|
||||
--bind="/:show-input+unbind(q)+unbind(j)+unbind(k)+unbind(l)+unbind(h)+unbind(ctrl-l)+unbind(ctrl-h)+unbind(alt-l)+unbind(alt-h)+unbind(load)+hide-preview+reload:$0 --reload-all" \
|
||||
--bind="backward-eof:hide-input+rebind(q)+rebind(j)+rebind(k)+rebind(l)+rebind(h)+rebind(ctrl-l)+rebind(ctrl-h)+rebind(alt-l)+rebind(alt-h)+rebind(load)+show-preview+reload:$0 --reload-week today" \
|
||||
--bind="esc:clear-query+hide-input+rebind(q)+rebind(j)+rebind(k)+rebind(l)+rebind(h)+rebind(ctrl-l)+rebind(ctrl-h)+rebind(alt-l)+rebind(alt-h)+rebind(load)+show-preview+reload:$0 --reload-week today"
|
||||
)
|
||||
|
||||
case "$selection" in
|
||||
"~"*)
|
||||
start=$(echo "$selection" | rev | cut -d';' -f 1 | rev)
|
||||
exec $0 --date "$start"
|
||||
;;
|
||||
*)
|
||||
day=$(echo "$selection" | rev | cut -d';' -f 1 | rev)
|
||||
exec $0 --day "$day"
|
||||
;;
|
||||
esac
|
||||
echo "Going to end..."
|
||||
echo "$selection"
|
||||
echo "STOPPING NOW"
|
||||
key=$(echo "$selection" | head -1)
|
||||
line=$(echo "$selection" | tail -1)
|
||||
if [ "$line" = "$key" ]; then
|
||||
line=""
|
||||
fi
|
||||
sign=$(echo "$line" | cut -f 1)
|
||||
DISPLAY_DATE=$(echo "$line" | cut -f 2)
|
||||
if [ "$key" = "ctrl-n" ]; then
|
||||
if [ "$sign" = "~" ]; then
|
||||
DISPLAY_DATE=""
|
||||
fi
|
||||
set -- "--new" "${DISPLAY_DATE:-today} $DAY_START:00"
|
||||
elif [ "$key" = "ctrl-g" ]; then
|
||||
set -- "--goto"
|
||||
elif [ "$key" = "ctrl-t" ]; then
|
||||
set -- "--set-tz" "$*"
|
||||
else
|
||||
if [ "$sign" = "~" ]; then
|
||||
set -- "--week" "$DISPLAY_DATE"
|
||||
else
|
||||
set -- "--day" "$DISPLAY_DATE"
|
||||
fi
|
||||
fi
|
||||
__export
|
||||
fi
|
||||
done
|
||||
|
121
src/sh/awkscripts.sh
Normal file
121
src/sh/awkscripts.sh
Normal file
@@ -0,0 +1,121 @@
|
||||
# AWK scripts
|
||||
# - AWK_APPROX: Generate approximate data of all files
|
||||
# - AWK_CALSHIFT: Shift calendar to start weeks on Mondays
|
||||
# - AWK_CALANNOT: Annotate calendar
|
||||
# - AWK_DAYVIEW: Generate view of the day
|
||||
# - AWK_GET: Print field of iCalendar file
|
||||
# - AWK_MERGE: Generate list of weeks with associated iCalendar files
|
||||
# - AWK_NEW: Make new iCalendar file
|
||||
# - AWK_PARSE: Timezone aware parsing of iCalendar file for day view
|
||||
# - AWK_SET: Set value of specific field in iCalendar file
|
||||
# - AWK_UPDATE: Update iCalendar file
|
||||
# - AWK_WEEKVIEW: Generate view of the week
|
||||
# - AWK_ATTACHLS: List attachments
|
||||
# - AWK_ATTACHDD: Store attachment
|
||||
# - AWK_ATTACHRM: Remove attachment
|
||||
# - AWK_ATTACH: Add attachment
|
||||
|
||||
AWK_APPROX=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/approx.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_APPROX
|
||||
|
||||
AWK_MERGE=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/merge.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_MERGE
|
||||
|
||||
AWK_PARSE=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/parse.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_PARSE
|
||||
|
||||
AWK_WEEKVIEW=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/weekview.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_WEEKVIEW
|
||||
|
||||
AWK_DAYVIEW=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/dayview.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_DAYVIEW
|
||||
|
||||
AWK_GET=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/get.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_GET
|
||||
|
||||
AWK_UPDATE=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/update.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_UPDATE
|
||||
|
||||
AWK_NEW=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/new.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_NEW
|
||||
|
||||
AWK_CALSHIFT=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/calshift.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_CALSHIFT
|
||||
|
||||
AWK_CALANNOT=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/calannot.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_CALANNOT
|
||||
|
||||
AWK_SET=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/set.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_SET
|
||||
|
||||
AWK_ATTACHLS=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/attachls.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_ATTACHLS
|
||||
|
||||
AWK_ATTACHDD=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/attachdd.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_ATTACHDD
|
||||
|
||||
AWK_ATTACHRM=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/attachrm.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_ATTACHRM
|
||||
|
||||
AWK_ATTACH=$(
|
||||
cat <<'EOF'
|
||||
@@include awk/attach.awk
|
||||
EOF
|
||||
)
|
||||
export AWK_ATTACH
|
129
src/sh/cliextra.sh
Normal file
129
src/sh/cliextra.sh
Normal file
@@ -0,0 +1,129 @@
|
||||
# Extra command-line options
|
||||
# - --import-ni
|
||||
# - --import
|
||||
# - --git
|
||||
# - --git-init
|
||||
|
||||
# Import iCalendar file noninteractively
|
||||
#
|
||||
# @input $2: Absolute path to iCalendar file
|
||||
# @input $3: Collection
|
||||
# @return: On success, returns 0, otherwise 1
|
||||
if [ "${1:-}" = "--import-ni" ]; then
|
||||
shift
|
||||
file="${1:-}"
|
||||
collection="${2:-}"
|
||||
if [ ! -f "$file" ]; then
|
||||
err "File \"$file\" does not exist"
|
||||
exit 1
|
||||
fi
|
||||
for c in $(echo "$COLLECTION_LABELS" | sed "s|=[^;]*;| |g"); do
|
||||
if [ "$collection" = "$c" ]; then
|
||||
cexists="yes"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "${cexists:-}" ] && [ -d "$ROOT/$collection" ]; then
|
||||
__import_to_collection "$file" "$collection"
|
||||
else
|
||||
err "Collection \"$collection\" does not exist"
|
||||
exit 1
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
# Import iCalendar file.
|
||||
#
|
||||
# @input $2: Absolute path to iCalendar file
|
||||
# @return: On success, returns 0, otherwise 1
|
||||
if [ "${1:-}" = "--import" ]; then
|
||||
shift
|
||||
file="${1:-}"
|
||||
if [ ! -f "$file" ]; then
|
||||
err "File \"$file\" does not exist"
|
||||
return 1
|
||||
fi
|
||||
line=$(awk \
|
||||
-v collection_labels="$COLLECTION_LABELS" \
|
||||
"$AWK_PARSE" "$file")
|
||||
set -- $line
|
||||
startsec="${1:-}"
|
||||
endsec="${2:-}"
|
||||
if [ -z "$line" ] || [ -z "$startsec" ] || [ -z "$endsec" ]; then
|
||||
err "File \"$file\" does not look like an iCalendar file containing an event"
|
||||
return 1
|
||||
fi
|
||||
start=$(__datetime_human_machine "$startsec")
|
||||
end=$(__datetime_human_machine "$endsec")
|
||||
location=$(awk -v field="LOCATION" "$AWK_GET" "$file")
|
||||
summary=$(awk -v field="SUMMARY" "$AWK_GET" "$file")
|
||||
description=$(awk -v field="DESCRIPTION" "$AWK_GET" "$file")
|
||||
filetmp=$(mktemp --suffix='.md')
|
||||
(
|
||||
echo "::: |> $start"
|
||||
echo "::: <| $end"
|
||||
) >"$filetmp"
|
||||
if [ -n "$location" ]; then
|
||||
echo "@ $location" >>"$filetmp"
|
||||
fi
|
||||
(
|
||||
echo "# $summary"
|
||||
echo ""
|
||||
echo "$description"
|
||||
) >>"$filetmp"
|
||||
$CAT "$filetmp" >/dev/tty
|
||||
while true; do
|
||||
printf "Do you want to import this entry? (yes/no): " >/dev/tty
|
||||
read -r yn
|
||||
case $yn in
|
||||
"yes")
|
||||
collection=$(echo "$COLLECTION_LABELS" | tr ';' '\n' | awk '/./ {print}' | $FZF --margin="30%" --no-info --delimiter='=' --with-nth=2 --accept-nth=1)
|
||||
if [ -z "$collection" ]; then
|
||||
exit
|
||||
fi
|
||||
__import_to_collection "$file" "$collection"
|
||||
break
|
||||
;;
|
||||
"no")
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Please answer \"yes\" or \"no\"." >/dev/tty
|
||||
;;
|
||||
esac
|
||||
done
|
||||
rm -f "$filetmp"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Run git command
|
||||
#
|
||||
# @input $2..: Git command
|
||||
# @return: On success, returns 0, otherwise 1
|
||||
if [ "${1:-}" = "--git" ]; then
|
||||
if [ -z "${GIT:-}" ]; then
|
||||
err "Git not supported, run \`$0 --git-init\` first"
|
||||
return 1
|
||||
fi
|
||||
shift
|
||||
$GIT "$@"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Enable the ues of git
|
||||
#
|
||||
# @return: On success, returns 0, otherwise 1
|
||||
if [ "${1:-}" = "--git-init" ]; then
|
||||
if [ -n "${GIT:-}" ]; then
|
||||
err "Git already enabled"
|
||||
return 1
|
||||
fi
|
||||
if ! command -v "git" >/dev/null; then
|
||||
err "Git command not found"
|
||||
return 1
|
||||
fi
|
||||
git -C "$ROOT" init
|
||||
git -C "$ROOT" add -A
|
||||
git -C "$ROOT" commit -m 'Initial commit: Start git tracking'
|
||||
exit
|
||||
fi
|
100
src/sh/clipreview.sh
Normal file
100
src/sh/clipreview.sh
Normal file
@@ -0,0 +1,100 @@
|
||||
# Preview command-line options
|
||||
# - --preview-event
|
||||
# - --preview_week
|
||||
|
||||
# Print preview of event and exit.
|
||||
#
|
||||
# @input $2: Line from day view containing an event
|
||||
if [ "${1:-}" = "--preview-event" ]; then
|
||||
hour=$(echo "$2" | cut -f 2)
|
||||
start=$(echo "$2" | cut -f 3)
|
||||
end=$(echo "$2" | cut -f 4)
|
||||
fpath=$(echo "$2" | cut -f 5)
|
||||
if [ -n "$hour" ] && [ -n "$fpath" ]; then
|
||||
fpath="$ROOT/$fpath"
|
||||
start=$(datetime_str "$start" "%a ")
|
||||
end=$(datetime_str "$end" "%a ")
|
||||
location=$(awk -v field="LOCATION" "$AWK_GET" "$fpath")
|
||||
status=$(awk -v field="STATUS" "$AWK_GET" "$fpath")
|
||||
if [ "$status" = "TENTATIVE" ]; then
|
||||
symb="🟡"
|
||||
elif [ "$status" = "CANCELLED" ]; then
|
||||
symb="❌"
|
||||
fi
|
||||
echo "📅${symb:-} ${STYLE_EPV_DATETIME}$start${OFF} → ${STYLE_EPV_DATETIME}$end${OFF}"
|
||||
if [ -n "${location:-}" ]; then
|
||||
echo "📍 ${STYLE_EPV_LOCATION}$location${OFF}"
|
||||
fi
|
||||
attcnt=$(awk "$AWK_ATTACHLS" "$fpath" | wc -l)
|
||||
if [ "$attcnt" -gt 0 ]; then
|
||||
echo "🔗 $attcnt attachments"
|
||||
fi
|
||||
echo ""
|
||||
awk -v field="DESCRIPTION" "$AWK_GET" "$fpath" | $CAT
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
# Print preview of week.
|
||||
#
|
||||
# @input $2: Line from week view
|
||||
if [ "${1:-}" = "--preview-week" ]; then
|
||||
sign=$(echo "$2" | cut -f 1)
|
||||
if [ "$sign" = "+" ]; then
|
||||
startdate=$(echo "$2" | cut -f 2)
|
||||
set -- $(date -d "$startdate" +"%Y %m %d")
|
||||
year=$1
|
||||
month=$2
|
||||
day=$3
|
||||
set -- $(date -d "today" +"%Y %m %d")
|
||||
year_cur=$1
|
||||
month_cur=$2
|
||||
day_cur=$3
|
||||
# Previous months
|
||||
set -- $(month_previous "$month" "$year")
|
||||
month_pre="$1"
|
||||
year_pre="$2"
|
||||
set -- $(month_previous "$month_pre" "$year_pre")
|
||||
month_pre2="$1"
|
||||
year_pre2="$2"
|
||||
# Next months
|
||||
set -- $(month_next "$month" "$year")
|
||||
month_nex="$1"
|
||||
year_nex="$2"
|
||||
set -- $(month_next "$month_nex" "$year_nex")
|
||||
month_nex2="$1"
|
||||
year_nex2="$2"
|
||||
set -- $(month_next "$month_nex2" "$year_nex2")
|
||||
month_nex3="$1"
|
||||
year_nex3="$2"
|
||||
# Highlight today
|
||||
if [ "$month_pre2" -eq "$month_cur" ] && [ "$year_pre2" -eq "$year_cur" ]; then
|
||||
var_pre2=$day_cur
|
||||
fi
|
||||
if [ "$month_pre" -eq "$month_cur" ] && [ "$year_pre" -eq "$year_cur" ]; then
|
||||
var_pre=$day_cur
|
||||
fi
|
||||
if [ "$month" -eq "$month_cur" ] && [ "$year" -eq "$year_cur" ]; then
|
||||
var=$day_cur
|
||||
fi
|
||||
if [ "$month_nex" -eq "$month_cur" ] && [ "$year_nex" -eq "$year_cur" ]; then
|
||||
var_nex=$day_cur
|
||||
fi
|
||||
if [ "$month_nex2" -eq "$month_cur" ] && [ "$year_nex2" -eq "$year_cur" ]; then
|
||||
var_nex2=$day_cur
|
||||
fi
|
||||
if [ "$month_nex3" -eq "$month_cur" ] && [ "$year_nex3" -eq "$year_cur" ]; then
|
||||
var_nex3=$day_cur
|
||||
fi
|
||||
# show
|
||||
(
|
||||
cal "$month_pre2" "$year_pre2" | awk "$AWK_CALSHIFT" | awk -v cur="${var_pre2:-}" -v style_month="$STYLE_CALENDAR_MONTH" -v style_weekdays="$STYLE_CALENDAR_WEEKDAYS" -v style_cur="$STYLE_CALENDAR_CURRENT_DAY" -v style_highlight="$STYLE_CALENDAR_HL_DAY" -v style_weekdays="$STYLE_CALENDAR_WEEKDAYS" -v style_cur="$STYLE_CALENDAR_CURRENT_DAY" -v style_highlight="$STYLE_CALENDAR_HL_DAY" "$AWK_CALANNOT"
|
||||
cal "$month_pre" "$year_pre" | awk "$AWK_CALSHIFT" | awk -v cur="${var_pre:-}" -v style_month="$STYLE_CALENDAR_MONTH" -v style_weekdays="$STYLE_CALENDAR_WEEKDAYS" -v style_cur="$STYLE_CALENDAR_CURRENT_DAY" -v style_highlight="$STYLE_CALENDAR_HL_DAY" "$AWK_CALANNOT"
|
||||
cal "$month" "$year" | awk "$AWK_CALSHIFT" | awk -v cur="${var:-}" -v day="$day" -v style_month="$STYLE_CALENDAR_MONTH" -v style_weekdays="$STYLE_CALENDAR_WEEKDAYS" -v style_cur="$STYLE_CALENDAR_CURRENT_DAY" -v style_highlight="$STYLE_CALENDAR_HL_DAY" "$AWK_CALANNOT"
|
||||
cal "$month_nex" "$year_nex" | awk "$AWK_CALSHIFT" | awk -v cur="${var_nex:-}" -v style_month="$STYLE_CALENDAR_MONTH" -v style_weekdays="$STYLE_CALENDAR_WEEKDAYS" -v style_cur="$STYLE_CALENDAR_CURRENT_DAY" -v style_highlight="$STYLE_CALENDAR_HL_DAY" "$AWK_CALANNOT"
|
||||
cal "$month_nex2" "$year_nex2" | awk "$AWK_CALSHIFT" | awk -v cur="${var_nex2:-}" -v style_month="$STYLE_CALENDAR_MONTH" -v style_weekdays="$STYLE_CALENDAR_WEEKDAYS" -v style_cur="$STYLE_CALENDAR_CURRENT_DAY" -v style_highlight="$STYLE_CALENDAR_HL_DAY" "$AWK_CALANNOT"
|
||||
cal "$month_nex3" "$year_nex3" | awk "$AWK_CALSHIFT" | awk -v cur="${var_nex3:-}" -v style_month="$STYLE_CALENDAR_MONTH" -v style_weekdays="$STYLE_CALENDAR_WEEKDAYS" -v style_cur="$STYLE_CALENDAR_CURRENT_DAY" -v style_highlight="$STYLE_CALENDAR_HL_DAY" "$AWK_CALANNOT"
|
||||
) | awk '{ l[(NR-1)%8] = l[(NR-1)%8] " " $0 } END {for (i in l) print l[i] }'
|
||||
fi
|
||||
exit
|
||||
fi
|
31
src/sh/clireload.sh
Normal file
31
src/sh/clireload.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
# Command-line Arguments for reloading views
|
||||
# - --reload-day
|
||||
# - --reload-week
|
||||
# - --reload-all
|
||||
|
||||
# Reload view of specified day.
|
||||
#
|
||||
# @input $2.. (optional): Specification of day, defaults to `today`
|
||||
if [ "${1:-}" = "--reload-day" ]; then
|
||||
shift
|
||||
DISPLAY_DATE=${*:-today}
|
||||
__view_day
|
||||
exit
|
||||
fi
|
||||
|
||||
# Reload view of the week containing the specified date.
|
||||
#
|
||||
# @input $2.. (optional): Specification of day, defaults to `today`
|
||||
if [ "${1:-}" = "--reload-week" ]; then
|
||||
shift
|
||||
DISPLAY_DATE=${*:-today}
|
||||
DISPLAY_POS=$((8 - $(date -d "$DISPLAY_DATE" +"%u")))
|
||||
__view_week
|
||||
exit
|
||||
fi
|
||||
|
||||
# Reload view of all entries.
|
||||
if [ "${1:-}" = "--reload-all" ]; then
|
||||
__view_all
|
||||
exit
|
||||
fi
|
66
src/sh/config.sh
Normal file
66
src/sh/config.sh
Normal file
@@ -0,0 +1,66 @@
|
||||
# Load Configuration
|
||||
# - ROOT: Directory containing the collections
|
||||
# - COLLECTION_LABELS: Mappings between collections and labels
|
||||
# - SYNC_CMD (optional): Synchronization command
|
||||
# - DAY_START (optional): Hour of start of the day (defaults to 8)
|
||||
# - DAY_END (optional): Hour of end of the day (defaults to 18)
|
||||
# - EDITOR (optional): Your favorite editor, is usually already exported
|
||||
# - TZ (optional): Your favorite timezone, usually system's choice
|
||||
# - LC_TIME (optional): Your favorite locale for date and time
|
||||
# - ZI_DIR (optional): Location of tzdata, defaults to /usr/share/zoneinfo
|
||||
|
||||
CONFIGFILE="$HOME/.config/fzf-vcal/config"
|
||||
if [ ! -f "$CONFIGFILE" ]; then
|
||||
err "Configuration '$CONFIGFILE' not found."
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck source=/dev/null
|
||||
. "$CONFIGFILE"
|
||||
if [ -z "${ROOT:-}" ] || [ -z "${COLLECTION_LABELS:-}" ]; then
|
||||
err "Configuration is incomplete."
|
||||
exit 1
|
||||
fi
|
||||
export ROOT COLLECTION_LABELS
|
||||
export SYNC_CMD=${SYNC_CMD:-echo 'Synchronization disabled'}
|
||||
export DAY_START=${DAY_START:-8}
|
||||
export DAY_END=${DAY_END:-18}
|
||||
export ZI_DIR=${ZI_DIR:-/usr/share/zoneinfo/posix}
|
||||
if [ ! -d "$ZI_DIR" ]; then
|
||||
err "Could not determine time-zone information"
|
||||
exit 1
|
||||
fi
|
||||
export OPEN=${OPEN:-open}
|
||||
|
||||
# Check and load required tools
|
||||
# - FZF: Fuzzy finder `fzf``
|
||||
# - UUIDGEN: Tool `uuidgen` to generate random uids
|
||||
# - CAT: `bat` or `batcat` or `cat`
|
||||
# - GIT: `git` if it exists
|
||||
#
|
||||
# The presence of POSIX tools is not checked.
|
||||
|
||||
if command -v "fzf" >/dev/null; then
|
||||
export FZF="fzf --black"
|
||||
else
|
||||
err "Did not find the command-line fuzzy finder fzf."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v "uuidgen" >/dev/null; then
|
||||
export UUIDGEN="uuidgen"
|
||||
else
|
||||
err "Did not find the uuidgen command."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v "bat" >/dev/null; then
|
||||
CAT="bat"
|
||||
elif command -v "batcat" >/dev/null; then
|
||||
CAT="batcat"
|
||||
fi
|
||||
CAT=${CAT:+$CAT --color=always --style=numbers --language=md}
|
||||
export CAT=${CAT:-cat}
|
||||
|
||||
if command -v "git" >/dev/null && [ -d "$ROOT/.git" ]; then
|
||||
export GIT="git -C $ROOT"
|
||||
fi
|
233
src/sh/icalendar.sh
Normal file
233
src/sh/icalendar.sh
Normal file
@@ -0,0 +1,233 @@
|
||||
# iCalendar modification wrapper
|
||||
# - __edit
|
||||
# - __new
|
||||
# - __delete
|
||||
# - __import_to_collection
|
||||
# - __cancel_toggle
|
||||
# - __tentative_toggle
|
||||
# - __add_attachment
|
||||
|
||||
# Edit iCalendar file.
|
||||
#
|
||||
# @input $1: Start date/date-time
|
||||
# @input $2: End date/date-time
|
||||
# @input $3: Path to iCalendar file (relative to `$ROOT`)
|
||||
__edit() {
|
||||
start=$(__datetime_human_machine "$1")
|
||||
end=$(__datetime_human_machine "$2")
|
||||
fpath="$ROOT/$3"
|
||||
location=$(awk -v field="LOCATION" "$AWK_GET" "$fpath" | tr -d "\n")
|
||||
summary=$(awk -v field="SUMMARY" "$AWK_GET" "$fpath" | tr -d "\n")
|
||||
description=$(awk -v field="DESCRIPTION" "$AWK_GET" "$fpath")
|
||||
filetmp=$(mktemp --suffix='.md')
|
||||
printf "::: |> %s\n::: <| %s\n" "$start" "$end" >"$filetmp"
|
||||
if [ -n "$location" ]; then
|
||||
printf "@ %s\n" "$location" >>"$filetmp"
|
||||
fi
|
||||
printf "# %s\n\n%s\n" "$summary" "$description" >>"$filetmp"
|
||||
checksum=$(cksum "$filetmp")
|
||||
$EDITOR "$filetmp" >/dev/tty
|
||||
|
||||
# Update only if changes are detected
|
||||
if [ "$checksum" != "$(cksum "$filetmp")" ]; then
|
||||
filenew="$filetmp.ics"
|
||||
if awk "$AWK_UPDATE" "$filetmp" "$fpath" >"$filenew"; then
|
||||
mv "$filenew" "$fpath"
|
||||
if [ -n "${GIT:-}" ]; then
|
||||
$GIT add "$fpath"
|
||||
$GIT commit -q -m "Modified event '$(__summary_for_commit "$fpath") ...'" -- "$fpath"
|
||||
fi
|
||||
__refresh_data
|
||||
else
|
||||
rm -f "$filenew"
|
||||
err "Failed to edit entry. Press <enter> to continue."
|
||||
read -r tmp
|
||||
fi
|
||||
fi
|
||||
rm "$filetmp"
|
||||
}
|
||||
|
||||
# Generate new iCalendar file
|
||||
#
|
||||
# This function also sets the `$start` variable to the start of the new entry.
|
||||
# On failure, start will be empty.
|
||||
#
|
||||
# If some start has been specified and the nanoseconds are not 0, we assume
|
||||
# that the user entered "tomorrow" or something like that, and did not
|
||||
# specify the time. So, we will use the `$DAY_START` time of that date.
|
||||
# If the user specified a malformed date/date-time, we fail.
|
||||
#
|
||||
# @input $1 (optional): Date or datetime, defaults to today.
|
||||
__new() {
|
||||
collection=$(echo "$COLLECTION_LABELS" | tr ';' '\n' | awk '/./ {print}' | $FZF --margin="30%" --no-info --delimiter='=' --with-nth=2 --accept-nth=1)
|
||||
fpath=""
|
||||
while [ -f "$fpath" ] || [ -z "$fpath" ]; do
|
||||
uuid=$($UUIDGEN)
|
||||
fpath="$ROOT/$collection/$uuid.ics"
|
||||
done
|
||||
d="today $DAY_START"
|
||||
if [ -n "${1:-}" ]; then
|
||||
d="$1"
|
||||
if [ "$(date -d "$1" +"%N")" -ne 0 ]; then
|
||||
d="$d $DAY_START:00"
|
||||
fi
|
||||
fi
|
||||
startsec=$(date -d "$d" +"%s")
|
||||
endsec=$((startsec + 3600))
|
||||
start=$(__datetime_human_machine "$startsec")
|
||||
end=$(__datetime_human_machine "$endsec")
|
||||
filetmp=$(mktemp --suffix='.md')
|
||||
(
|
||||
echo "::: |> $start"
|
||||
echo "::: <| $end"
|
||||
echo "@ <!-- write location here, optional line -->"
|
||||
echo "# <!-- write summary here -->"
|
||||
echo ""
|
||||
) >"$filetmp"
|
||||
checksum=$(cksum "$filetmp")
|
||||
$EDITOR "$filetmp" >/dev/tty
|
||||
|
||||
# Update only if changes are detected
|
||||
if [ "$checksum" != "$(cksum "$filetmp")" ]; then
|
||||
filenew="$filetmp.ics"
|
||||
if awk -v uid="$uuid" "$AWK_NEW" "$filetmp" >"$filenew"; then
|
||||
mv "$filenew" "$fpath"
|
||||
if [ -n "${GIT:-}" ]; then
|
||||
$GIT add "$fpath"
|
||||
$GIT commit -q -m "Added event '$(__summary_for_commit "$fpath") ...'" -- "$fpath"
|
||||
fi
|
||||
start=$(awk -v field="DTSTART" "$AWK_GET" "$fpath" | grep -o '[0-9]\{8\}')
|
||||
else
|
||||
rm -f "$filenew"
|
||||
start=""
|
||||
err "Failed to create new entry. Press <enter> to continue."
|
||||
read -r tmp
|
||||
fi
|
||||
fi
|
||||
rm "$filetmp"
|
||||
}
|
||||
|
||||
# Delete iCalendar file
|
||||
#
|
||||
# @input $1: Path to iCalendar file, relative to `$ROOT`
|
||||
__delete() {
|
||||
fpath="$ROOT/$1"
|
||||
summary=$(awk -v field="SUMMARY" "$AWK_GET" "$fpath")
|
||||
while true; do
|
||||
printf "Do you want to delete the entry with the title \"%s\"? (yes/no): " "$summary" >/dev/tty
|
||||
read -r yn
|
||||
case $yn in
|
||||
"yes")
|
||||
sfg="$(__summary_for_commit "$fpath")"
|
||||
rm -v "$fpath"
|
||||
if [ -n "${GIT:-}" ]; then
|
||||
$GIT add "$fpath"
|
||||
$GIT commit -q -m "Deleted event '$sfg ...'" -- "$fpath"
|
||||
fi
|
||||
break
|
||||
;;
|
||||
"no")
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Please answer \"yes\" or \"no\"." >/dev/tty
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Import iCalendar file to specified collection. The only modification made to
|
||||
# the file is setting the UID.
|
||||
#
|
||||
# @input $1: path to iCalendar file
|
||||
# @input $2: collection name
|
||||
__import_to_collection() {
|
||||
file="$1"
|
||||
collection="$2"
|
||||
fpath=""
|
||||
while [ -f "$fpath" ] || [ -z "$fpath" ]; do
|
||||
uuid=$($UUIDGEN)
|
||||
fpath="$ROOT/$collection/$uuid.ics"
|
||||
done
|
||||
filetmp=$(mktemp)
|
||||
awk -v field="UID" -v value="$uuid" "$AWK_SET" "$file" >"$filetmp"
|
||||
mv "$filetmp" "$fpath"
|
||||
if [ -n "${GIT:-}" ]; then
|
||||
$GIT add "$fpath"
|
||||
$GIT commit -q -m "Imported event '$(__summary_for_commit "$fpath") ...'" -- "$fpath"
|
||||
fi
|
||||
}
|
||||
|
||||
# Set status of appointment to CANCELLED or CONFIRMED (toggle)
|
||||
#
|
||||
# @input $1: path to iCalendar file
|
||||
__cancel_toggle() {
|
||||
fpath="$ROOT/$1"
|
||||
status=$(awk -v field="STATUS" "$AWK_GET" "$fpath")
|
||||
newstatus="CANCELLED"
|
||||
if [ "${status:-}" = "$newstatus" ]; then
|
||||
newstatus="CONFIRMED"
|
||||
fi
|
||||
filetmp=$(mktemp)
|
||||
awk -v field="STATUS" -v value="$newstatus" "$AWK_SET" "$fpath" >"$filetmp"
|
||||
mv "$filetmp" "$fpath"
|
||||
if [ -n "${GIT:-}" ]; then
|
||||
$GIT add "$fpath"
|
||||
$GIT commit -q -m "Event '$(__summary_for_commit "$fpath") ...' has now status $status" -- "$fpath"
|
||||
fi
|
||||
}
|
||||
|
||||
# Toggle status flag: CONFIRMED <-> TENTATIVE
|
||||
#
|
||||
# @input $1: path to iCalendar file
|
||||
__tentative_toggle() {
|
||||
fpath="$ROOT/$1"
|
||||
status=$(awk -v field="STATUS" "$AWK_GET" "$fpath")
|
||||
newstatus="TENTATIVE"
|
||||
if [ "${status:-}" = "$newstatus" ]; then
|
||||
newstatus="CONFIRMED"
|
||||
fi
|
||||
filetmp=$(mktemp)
|
||||
awk -v field="STATUS" -v value="$newstatus" "$AWK_SET" "$fpath" >"$filetmp"
|
||||
mv "$filetmp" "$fpath"
|
||||
if [ -n "${GIT:-}" ]; then
|
||||
$GIT add "$fpath"
|
||||
$GIT commit -q -m "Event '$(__summary_for_commit "$fpath") ...' has now status $status" -- "$fpath"
|
||||
fi
|
||||
}
|
||||
|
||||
# Prepend attachment to iCalendar file
|
||||
#
|
||||
# @input $1: path to iCalendar file
|
||||
__add_attachment() {
|
||||
fpath="$ROOT/$1"
|
||||
sel=$(
|
||||
$FZF --prompt="Select attachment> " \
|
||||
--walker="file,hidden" \
|
||||
--walker-root="$HOME" \
|
||||
--expect="ctrl-c,ctrl-g,ctrl-q,esc"
|
||||
)
|
||||
key=$(echo "$sel" | head -1)
|
||||
f=$(echo "$sel" | tail -1)
|
||||
if [ -n "$key" ]; then
|
||||
f=""
|
||||
fi
|
||||
if [ -z "$f" ] || [ ! -f "$f" ]; then
|
||||
return
|
||||
fi
|
||||
filename=$(basename "$f")
|
||||
mime=$(file -b -i "$f" | cut -d ';' -f 1)
|
||||
if [ -z "$mime" ]; then
|
||||
mime="application/octet-stream"
|
||||
fi
|
||||
fenc=$(mktemp)
|
||||
base64 "$f" >"$fenc"
|
||||
filetmp=$(mktemp)
|
||||
awk -v file="$fenc" -v mime="$mime" -v filename="$filename" "$AWK_ATTACH" "$fpath" >"$filetmp"
|
||||
mv "$filetmp" "$fpath"
|
||||
if [ -n "${GIT:-}" ]; then
|
||||
$GIT add "$fpath"
|
||||
$GIT commit -q -m "Added attachment to '$(__summary_for_commit "$fpath") ...'" -- "$fpath"
|
||||
fi
|
||||
rm "$fenc"
|
||||
}
|
45
src/sh/load.sh
Normal file
45
src/sh/load.sh
Normal file
@@ -0,0 +1,45 @@
|
||||
# Loading functions
|
||||
# - __load_approx_data
|
||||
# - __load_weeks
|
||||
# - __refresh_data
|
||||
|
||||
# Print approximate data from iCalendar files in `$ROOT`
|
||||
__load_approx_data() {
|
||||
find "$ROOT" -type f -name '*.ics' -print0 |
|
||||
xargs -0 -P0 \
|
||||
awk \
|
||||
-v collection_labels="$COLLECTION_LABELS" \
|
||||
-v style_line="$STYLE_LV" \
|
||||
"$AWK_APPROX"
|
||||
}
|
||||
|
||||
# For every relevant week, print associated iCalendar files
|
||||
__load_weeks() {
|
||||
dates=$(awk -F'\t' '{ print $2; print $3 }' "$APPROX_DATA_FILE")
|
||||
file_dates=$(mktemp)
|
||||
echo "$dates" | date --file="/dev/stdin" +"%G:%V:" >"$file_dates"
|
||||
awk "$AWK_MERGE" "$file_dates" "$APPROX_DATA_FILE"
|
||||
rm "$file_dates"
|
||||
}
|
||||
|
||||
# Refresh approximate data and per-week data.
|
||||
#
|
||||
# This functions stores the output of `__load_approx_data` in the temporary
|
||||
# file `$APPROX_DATA_FILE` and the output of `__load_weeks` in the temporary
|
||||
# file `@WEEKLY_DATA_FILE`.
|
||||
__refresh_data() {
|
||||
if [ -z "${APPROX_DATA_FILE:-}" ]; then
|
||||
APPROX_DATA_FILE=$(mktemp)
|
||||
trap 'rm -f "$APPROX_DATA_FILE"' EXIT INT
|
||||
fi
|
||||
if [ -z "${WEEKLY_DATA_FILE:-}" ]; then
|
||||
WEEKLY_DATA_FILE=$(mktemp)
|
||||
trap 'rm -f "$WEEKLY_DATA_FILE"' EXIT INT
|
||||
fi
|
||||
debug "__refresh_data(): going to load approx data"
|
||||
__load_approx_data >"$APPROX_DATA_FILE"
|
||||
debug "__refresh_data(): approx data loaded"
|
||||
debug "__refresh_data(): going to load weeks"
|
||||
__load_weeks >"$WEEKLY_DATA_FILE"
|
||||
debug "__refresh_data(): weeks loaded"
|
||||
}
|
44
src/sh/misc.sh
Normal file
44
src/sh/misc.sh
Normal file
@@ -0,0 +1,44 @@
|
||||
# err()
|
||||
# This is a helper function to print errors.
|
||||
#
|
||||
# @input $1: Error message
|
||||
err() {
|
||||
echo "❌ $1" >/dev/tty
|
||||
}
|
||||
|
||||
# debug()
|
||||
# Pring debug message to fzf-vcal.debug
|
||||
#
|
||||
# @input $1: Debug message
|
||||
debug() {
|
||||
echo "$(date +"%D %T.%N"): $1" >>"/tmp/fzf-vcal.debug"
|
||||
}
|
||||
|
||||
# Print date or datetime in a human and machine readable form.
|
||||
#
|
||||
# @input $1: Seconds since epoch
|
||||
__datetime_human_machine() {
|
||||
s="$1"
|
||||
t=$(date -d "@$s" +"%R")
|
||||
dfmt="%F"
|
||||
if [ "$t" != "00:00" ]; then
|
||||
dfmt="$dfmt %R"
|
||||
fi
|
||||
date -d "@$s" +"$dfmt"
|
||||
}
|
||||
|
||||
# Get summary string that can be used in for git-commit messages.
|
||||
#
|
||||
# @input $1: iCalendar file path
|
||||
__summary_for_commit() {
|
||||
awk -v field="SUMMARY" "$AWK_GET" "$1" | tr -c -d "[:alnum:][:blank:]" | head -c 15
|
||||
}
|
||||
|
||||
# Re-export dynamical variables to subshells.
|
||||
__export() {
|
||||
DISPLAY_DATE=$(date -R -d "$DISPLAY_DATE")
|
||||
export DISPLAY_DATE
|
||||
if [ -n "${TZ:-}" ]; then
|
||||
export TZ
|
||||
fi
|
||||
}
|
51
src/sh/preview.sh
Normal file
51
src/sh/preview.sh
Normal file
@@ -0,0 +1,51 @@
|
||||
# Preview helper functions
|
||||
# - month_previous
|
||||
# - month_next
|
||||
# - datetime_str
|
||||
|
||||
# Print previous month of specified input month as <month> <year>.
|
||||
#
|
||||
# @input $1: Month
|
||||
# @input $2: Year
|
||||
month_previous() {
|
||||
month=$(echo "$1" | sed 's/^0//')
|
||||
year=$(echo "$2" | sed 's/^0//')
|
||||
if [ "$month" -eq 1 ]; then
|
||||
month=12
|
||||
year=$((year - 1))
|
||||
else
|
||||
month=$((month - 1))
|
||||
fi
|
||||
echo "$month $year"
|
||||
}
|
||||
|
||||
# Print next month of specified input month as <month> <year>.
|
||||
#
|
||||
# @input $1: Month
|
||||
# @input $2: Year
|
||||
month_next() {
|
||||
month=$(echo "$1" | sed 's/^0//')
|
||||
year=$(echo "$2" | sed 's/^0//')
|
||||
if [ "$month" -eq 12 ]; then
|
||||
month=1
|
||||
year=$((year + 1))
|
||||
else
|
||||
month=$((month + 1))
|
||||
fi
|
||||
echo "$month $year"
|
||||
}
|
||||
|
||||
# Print date or datetime in a human readable form.
|
||||
#
|
||||
# @input $1: Seconds since epoch
|
||||
# @input $2.. (optoinal): Prepend date format
|
||||
datetime_str() {
|
||||
s="$1"
|
||||
shift
|
||||
t=$(date -d "@$s" +"%R")
|
||||
dfmt="$*%e %b %Y"
|
||||
if [ "$t" != "00:00" ]; then
|
||||
dfmt="$dfmt %R %Z"
|
||||
fi
|
||||
date -d "@$s" +"$dfmt"
|
||||
}
|
47
src/sh/theme.sh
Normal file
47
src/sh/theme.sh
Normal file
@@ -0,0 +1,47 @@
|
||||
# Colors
|
||||
GREEN="\033[1;32m"
|
||||
BLACK="\033[1;30m"
|
||||
RED="\033[1;31m"
|
||||
WHITE="\033[1;97m"
|
||||
CYAN="\033[1;36m"
|
||||
LIGHT_CYAN="\033[1;36m"
|
||||
STRIKE="\033[9m"
|
||||
ITALIC="\033[3m"
|
||||
FAINT="\033[2m"
|
||||
BOLD="\033[1m"
|
||||
BG="\033[41m"
|
||||
|
||||
export OFF="\033[m"
|
||||
|
||||
# Style
|
||||
# Calendar
|
||||
export STYLE_CALENDAR_MONTH="${STYLE_CALENDAR_MONTH:-$GREEN}"
|
||||
export STYLE_CALENDAR_WEEKDAYS="${STYLE_CALENDAR_WEEKDAYS:-$FAINT}"
|
||||
export STYLE_CALENDAR_CURRENT_DAY="${STYLE_CALENDAR_CURRENT_DAY:-$BLACK$BG}"
|
||||
export STYLE_CALENDAR_HL_DAY="${STYLE_CALENDAR_HL_DAY:-$BOLD$RED}"
|
||||
|
||||
# Week view
|
||||
export STYLE_WV_DAY="${STYLE_WV_DAY:-$GREEN}"
|
||||
export STYLE_WV_EVENT_DELIM="${STYLE_WV_EVENT_DELIM:-$RED / $OFF}"
|
||||
export STYLE_WV_SUMMARY="${STYLE_WV_SUMMARY:-$CYAN}"
|
||||
export STYLE_WV_TIME="${STYLE_WV_TIME:-$WHITE}"
|
||||
export STYLE_WV_CONFIRMED="${STYLE_WV_CONFIRMED:-$CYAN}"
|
||||
export STYLE_WV_TENTATIVE="${STYLE_WV_TENTATIVE:-$FAINT$CYAN}"
|
||||
export STYLE_WV_CANCELLED="${STYLE_WV_CANCELLED:-$STRIKE$FAINT$CYAN}"
|
||||
|
||||
# List view
|
||||
export STYLE_LV="${STYLE_LV:-$FAINT}"
|
||||
|
||||
# Day view
|
||||
export STYLE_DV_ALLDAY="${STYLE_DV_ALLDAY:-$LIGHT_CYAN$ITALIC$FAINT (allday) $OFF}"
|
||||
export STYLE_DV_TIME="${STYLE_DV_TIME:-$LIGHT_CYAN}"
|
||||
export STYLE_DV_CONFIRMED="${STYLE_DV_CONFIRMED:-$CYAN}"
|
||||
export STYLE_DV_TENTATIVE="${STYLE_DV_TENTATIVE:-$FAINT$CYAN}"
|
||||
export STYLE_DV_CANCELLED="${STYLE_DV_CANCELLED:-$STRIKE$FAINT$CYAN}"
|
||||
export STYLE_DV_HOUR="${STYLE_DV_HOUR:-$FAINT}"
|
||||
export STYLE_DV_EMPTYHOUR="${STYLE_DV_EMPTYHOUR:-$FAINT----------------------$OFF}"
|
||||
export STYLE_DV_TZ="$WHITE$ITALIC"
|
||||
|
||||
# Event preview
|
||||
export STYLE_EPV_DATETIME="${STYLE_EPV_DATETIME:-$CYAN}"
|
||||
export STYLE_EPV_LOCATION="${STYLE_EPV_LOCATION:-$GREEN}"
|
157
src/sh/view.sh
Normal file
157
src/sh/view.sh
Normal file
@@ -0,0 +1,157 @@
|
||||
# View Functions
|
||||
# - __view_day
|
||||
# - __view_week
|
||||
# - __view_all
|
||||
|
||||
# This function prints the view for the day specified in `$DISPLAY_DATE`, in
|
||||
# the tab-delimited format with the fields:
|
||||
# 1. start date
|
||||
# 2. start time
|
||||
# 3. end time
|
||||
# 4. file path
|
||||
# 5. collection
|
||||
# 6. description
|
||||
__view_day() {
|
||||
weeknr=$(date -d "$DISPLAY_DATE" +"%G:%V:")
|
||||
files=$(grep "^$weeknr" "$WEEKLY_DATA_FILE" | cut -f 2)
|
||||
# Find relevant files in list of week files
|
||||
sef=$({
|
||||
set -- $files
|
||||
for file in "$@"; do
|
||||
file="$ROOT/$file"
|
||||
awk \
|
||||
-v collection_labels="$COLLECTION_LABELS" \
|
||||
"$AWK_PARSE" "$file"
|
||||
done
|
||||
})
|
||||
# $sef holds (space-delimited): <start> <end> <fpath> <collection> <status> <summary>
|
||||
today=$(date -d "$DISPLAY_DATE" +"%D")
|
||||
if [ -n "$sef" ]; then
|
||||
sef=$(echo "$sef" | while IFS= read -r line; do
|
||||
set -- $line
|
||||
starttime="$1"
|
||||
shift
|
||||
endtime="$1"
|
||||
shift
|
||||
fpath="$1" # we will use | as delimiter (need to convert back!)
|
||||
shift
|
||||
collection="$1"
|
||||
shift
|
||||
status="$1"
|
||||
shift
|
||||
description="$*"
|
||||
#
|
||||
daystart=$(date -d "$today 00:00:00" +"%s")
|
||||
dayend=$(date -d "$today 23:59:59" +"%s")
|
||||
line=""
|
||||
if [ "$starttime" -gt "$daystart" ] && [ "$starttime" -lt "$dayend" ]; then
|
||||
s=$(date -d "@$starttime" +"%R")
|
||||
elif [ "$starttime" -le "$daystart" ] && [ "$endtime" -gt "$daystart" ]; then
|
||||
s="00:00"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
if [ "$endtime" -gt "$daystart" ] && [ "$endtime" -lt "$dayend" ]; then
|
||||
e=$(date -d "@$endtime" +"%R")
|
||||
elif [ "$endtime" -ge "$dayend" ] && [ "$starttime" -lt "$dayend" ]; then
|
||||
e="00:00"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "$s" "$e" "$starttime" "$endtime" "$fpath" "$collection" "$description" "$status"
|
||||
done)
|
||||
fi
|
||||
echo "$sef" | sort -n | awk \
|
||||
-v today="$today" \
|
||||
-v daystart="$DAY_START" \
|
||||
-v dayend="$DAY_END" \
|
||||
-v style_allday="$STYLE_DV_ALLDAY" \
|
||||
-v style_timerange="$STYLE_DV_TIME" \
|
||||
-v style_confirmed="$STYLE_DV_CONFIRMED" \
|
||||
-v style_tentative="$STYLE_DV_TENTATIVE" \
|
||||
-v style_cancelled="$STYLE_DV_CANCELLED" \
|
||||
-v style_hour="$STYLE_DV_HOUR" \
|
||||
-v style_emptyhour="$STYLE_DV_EMPTYHOUR" \
|
||||
"$AWK_DAYVIEW"
|
||||
}
|
||||
|
||||
# This function prints the view for the week that contains the day specified in `$DISPLAY_DATE`.
|
||||
__view_week() {
|
||||
debug "__view_week(): Enter"
|
||||
weeknr=$(date -d "$DISPLAY_DATE" +"%G:%V:")
|
||||
files=$(grep "^$weeknr" "$WEEKLY_DATA_FILE" | cut -f 2)
|
||||
dayofweek=$(date -d "$DISPLAY_DATE" +"%u")
|
||||
delta=$((1 - dayofweek))
|
||||
startofweek=$(date -d "$DISPLAY_DATE -$delta days" +"%D")
|
||||
# loop over files
|
||||
debug "__view_week(): loop over files"
|
||||
sef=$({
|
||||
printf "%s" "$files" | xargs -d " " -I {} -P0 \
|
||||
awk \
|
||||
-v collection_labels="$COLLECTION_LABELS" \
|
||||
"$AWK_PARSE" "$ROOT/{}"
|
||||
})
|
||||
debug "__view_week(): loop over files ended"
|
||||
debug "__view_week(): prepare week view"
|
||||
if [ -n "$sef" ]; then
|
||||
sef=$(echo "$sef" | while IFS= read -r line; do
|
||||
set -- $line
|
||||
starttime="$1"
|
||||
shift
|
||||
endtime="$1"
|
||||
shift
|
||||
#fpath="$1"
|
||||
shift
|
||||
collection="$1"
|
||||
shift
|
||||
status="$1"
|
||||
shift
|
||||
if [ "$status" = "TENTATIVE" ]; then
|
||||
symb="$STYLE_WV_TENTATIVE"
|
||||
elif [ "$status" = "CANCELLED" ]; then
|
||||
symb="$STYLE_WV_CANCELLED"
|
||||
else
|
||||
symb="$STYLE_WV_CONFIRMED"
|
||||
fi
|
||||
description="${symb:-}$*$OFF"
|
||||
for i in $(seq 0 7); do
|
||||
daystart=$(date -d "$startofweek +$i days 00:00:00" +"%s")
|
||||
dayend=$(date -d "$startofweek +$i days 23:59:59" +"%s")
|
||||
if [ "$starttime" -gt "$daystart" ] && [ "$starttime" -lt "$dayend" ]; then
|
||||
s=$(date -d "@$starttime" +"%H:%M")
|
||||
elif [ "$starttime" -le "$daystart" ] && [ "$endtime" -gt "$daystart" ]; then
|
||||
s="00:00"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
if [ "$endtime" -gt "$daystart" ] && [ "$endtime" -lt "$dayend" ]; then
|
||||
e=$(date -d "@$endtime" +"%H:%M")
|
||||
elif [ "$endtime" -ge "$dayend" ] && [ "$starttime" -lt "$dayend" ]; then
|
||||
e="00:00"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
printf "%s\t%s\t%s\t%s\n" "$i" "$s" "$e" "$description"
|
||||
done
|
||||
done)
|
||||
fi
|
||||
debug "__view_week(): prepare week view ended"
|
||||
debug "__view_week(): generate week view"
|
||||
sef=$({
|
||||
echo "$sef"
|
||||
seq 0 7
|
||||
} | sort -n)
|
||||
echo "$sef" | awk \
|
||||
-v startofweek="$startofweek" \
|
||||
-v style_day="$STYLE_WV_DAY" \
|
||||
-v style_event_delim="$STYLE_WV_EVENT_DELIM" \
|
||||
-v style_summary="$STYLE_WV_SUMMARY" \
|
||||
-v style_time="$STYLE_WV_TIME" \
|
||||
"$AWK_WEEKVIEW"
|
||||
debug "__view_week(): generate week view ended"
|
||||
}
|
||||
|
||||
# This function prints all entries.
|
||||
__view_all() {
|
||||
cat "$APPROX_DATA_FILE"
|
||||
}
|
Reference in New Issue
Block a user