improvement: externalized files

This commit is contained in:
Ämin Baumeler 2025-06-18 13:49:56 +02:00
parent 8970e89cc0
commit 5a3668d6a9
20 changed files with 1114 additions and 1339 deletions

View File

@ -5,7 +5,15 @@ GREEN="\033[0;32m"
OFF="\033[m" OFF="\033[m"
NAME="fzf-vcal" NAME="fzf-vcal"
SRC="./src/main.sh" 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" chmod +x "$NAME"
echo "🥚 ${GREEN}Done${OFF}" rm -rf "$tmpdir"
echo "🍳 ${GREEN}Done:${OFF} Sucessfully built ${BOLD}${GREEN}$NAME${OFF}"

View File

@ -3,80 +3,10 @@
## ##
## @assign collection_labels: See configuration of the current program. ## @assign collection_labels: See configuration of the current program.
@include "lib/awk/icalendar.awk"
# Functions # Functions
# 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 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 the date/date-time entry at the current record.
#
# @local variables: dt
# @return: date or date-time string that can be used in date (1)
function parse( dt) {
# Get timezone information
for (i=2; i<NF-1; i+=2) {
if ($i == "TZID") {
dt = "TZ=\"" $(i+1) "\" "
break
}
}
# Get date/date-time
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)
}
# 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
}
# Get relative file path. # Get relative file path.
# #
# @local variables: n, a # @local variables: n, a

View File

@ -3,41 +3,8 @@
## ##
## @assign field: Field name ## @assign field: Field name
# Unescape string @include "lib/awk/icalendar.awk"
#
# @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 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))
}
# AWK program
BEGIN { FS = ":"; regex = "^" field } BEGIN { FS = ":"; regex = "^" field }
/^BEGIN:VEVENT$/ { inside = 1 } /^BEGIN:VEVENT$/ { inside = 1 }
/^END:VEVENT$/ { exit } /^END:VEVENT$/ { exit }

View File

@ -3,40 +3,7 @@
## ##
## @assign uid: UID to use ## @assign uid: UID to use
# Functions @include "lib/awk/icalendar.awk"
# Escape string to be used as content in iCalendar files.
#
# @input str: String to escape
# @return: Escaped string
function escape(str)
{
gsub("\\\\", "\\\\", 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
}
}
# AWK program # AWK program
BEGIN { BEGIN {

View File

@ -6,79 +6,7 @@
## ##
## @assign collection_labels: See configuration of the current program. ## @assign collection_labels: See configuration of the current program.
# Functions @include "lib/awk/icalendar.awk"
# 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 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 the date/date-time entry at the current record.
#
# @local variables: dt
# @return: date or date-time string that can be used in date (1)
function parse( dt) {
# Get timezone information
for (i=2; i<NF-1; i+=2) {
if ($i == "TZID") {
dt = "TZ=\"" $(i+1) "\" "
break
}
}
# Get date/date-time
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)
}
# 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
}
# Print string of parsed data. # Print string of parsed data.
# #

View File

@ -6,21 +6,7 @@
## ##
## LIMITATION: This program does not fold long content lines. ## LIMITATION: This program does not fold long content lines.
# Functions @include "lib/awk/icalendar.awk"
# Escape string to be used as content.
#
# @input str: Content string
# @return: Escaped string
function escape(str)
{
gsub("\\\\", "\\\\", str)
gsub(";", "\\;", str)
gsub(",", "\\,", str)
return str
}
# AWK program
BEGIN { FS = "[:;]"; zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1) } BEGIN { FS = "[:;]"; zulu = strftime("%Y%m%dT%H%M%SZ", systime(), 1) }
/^BEGIN:VEVENT$/ { inside = 1 } /^BEGIN:VEVENT$/ { inside = 1 }

View File

@ -1,42 +1,7 @@
## src/awk/update.awk ## src/awk/update.awk
## Update iCalendar file from markdown file. ## Update iCalendar file from markdown file.
# Functions @include "lib/awk/icalendar.awk"
# Escape string to be used as content in iCalendar files.
#
# @input str: String to escape
# @return: Escaped string
function escape(str)
{
gsub("\\\\", "\\\\", 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
}
}
# AWK program
BEGIN { BEGIN {
FS=":" FS=":"

104
src/lib/awk/icalendar.awk Normal file
View File

@ -0,0 +1,104 @@
# Escape string to be used as content in iCalendar files.
#
# @input str: String to escape
# @return: Escaped string
function escape(str)
{
gsub("\\\\", "\\\\", 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 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 the date/date-time entry at the current record.
#
# @local variables: dt
# @return: date or date-time string that can be used in date (1)
function parse( dt) {
# Get timezone information
for (i=2; i<NF-1; i+=2) {
if ($i == "TZID") {
dt = "TZ=\"" $(i+1) "\" "
break
}
}
# Get date/date-time
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)
}
# 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
}

File diff suppressed because it is too large Load Diff

106
src/sh/awkscripts.sh Normal file
View File

@ -0,0 +1,106 @@
# 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
)
AWK_MERGE=$(
cat <<'EOF'
@@include awk/merge.awk
EOF
)
AWK_PARSE=$(
cat <<'EOF'
@@include awk/parse.awk
EOF
)
AWK_WEEKVIEW=$(
cat <<'EOF'
@@include awk/weekview.awk
EOF
)
AWK_DAYVIEW=$(
cat <<'EOF'
@@include awk/dayview.awk
EOF
)
AWK_GET=$(
cat <<'EOF'
@@include awk/get.awk
EOF
)
AWK_UPDATE=$(
cat <<'EOF'
@@include awk/update.awk
EOF
)
AWK_NEW=$(
cat <<'EOF'
@@include awk/new.awk
EOF
)
AWK_CALSHIFT=$(
cat <<'EOF'
@@include awk/calshift.awk
EOF
)
AWK_CALANNOT=$(
cat <<'EOF'
@@include awk/calannot.awk
EOF
)
AWK_SET=$(
cat <<'EOF'
@@include awk/set.awk
EOF
)
AWK_ATTACHLS=$(
cat <<'EOF'
@@include awk/attachls.awk
EOF
)
AWK_ATTACHDD=$(
cat <<'EOF'
@@include awk/attachdd.awk
EOF
)
AWK_ATTACHRM=$(
cat <<'EOF'
@@include awk/attachrm.awk
EOF
)
AWK_ATTACH=$(
cat <<'EOF'
@@include awk/attach.awk
EOF
)

129
src/sh/cliextra.sh Normal file
View 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
View 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 -d '|' -f 2)
start=$(echo "$2" | cut -d '|' -f 3)
end=$(echo "$2" | cut -d '|' -f 4)
fpath=$(echo "$2" | cut -d '|' -f 5 | sed "s/ /|/g")
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:-} ${CYAN}$start${OFF}${CYAN}$end${OFF}"
if [ -n "${location:-}" ]; then
echo "📍 ${CYAN}$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 -d '|' -f 1)
if [ "$sign" = "+" ]; then
startdate=$(echo "$2" | cut -d '|' -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:-}" "$AWK_CALANNOT"
cal "$month_pre" "$year_pre" | awk "$AWK_CALSHIFT" | awk -v cur="${var_pre:-}" "$AWK_CALANNOT"
cal "$month" "$year" | awk "$AWK_CALSHIFT" | awk -v cur="${var:-}" -v day="$day" "$AWK_CALANNOT"
cal "$month_nex" "$year_nex" | awk "$AWK_CALSHIFT" | awk -v cur="${var_nex:-}" "$AWK_CALANNOT"
cal "$month_nex2" "$year_nex2" | awk "$AWK_CALSHIFT" | awk -v cur="${var_nex2:-}" "$AWK_CALANNOT"
cal "$month_nex3" "$year_nex3" | awk "$AWK_CALSHIFT" | awk -v cur="${var_nex3:-}" "$AWK_CALANNOT"
) | awk '{ l[NR%8] = l[NR%8] " " $0 } END {for (i in l) if (i>0) print l[i] }'
fi
exit
fi

31
src/sh/clireload.sh Normal file
View 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

65
src/sh/config.sh Normal file
View File

@ -0,0 +1,65 @@
# 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
SYNC_CMD=${SYNC_CMD:-echo 'Synchronization disabled'}
DAY_START=${DAY_START:-8}
DAY_END=${DAY_END:-18}
ZI_DIR=${ZI_DIR:-/usr/share/zoneinfo/posix}
if [ ! -d "$ZI_DIR" ]; then
err "Could not determine time-zone information"
exit 1
fi
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
FZF="fzf --black"
else
err "Did not find the command-line fuzzy finder fzf."
exit 1
fi
if command -v "uuidgen" >/dev/null; then
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}
CAT=${CAT:-cat}
if command -v "git" >/dev/null && [ -d "$ROOT/.git" ]; then
GIT="git -C $ROOT"
fi

256
src/sh/icalendar.sh Normal file
View File

@ -0,0 +1,256 @@
# 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`)
# @req $AWK_GET: Awk script to extract fields from iCalendar file
# @req $AWK_UPDATE: Awk script to update iCalendar file
# @req $EDITOR: Environment variable of your favorite editor
__edit() {
start=$(__datetime_human_machine "$1")
end=$(__datetime_human_machine "$2")
fpath="$ROOT/$3"
location=$(awk -v field="LOCATION" "$AWK_GET" "$fpath")
summary=$(awk -v field="SUMMARY" "$AWK_GET" "$fpath")
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 -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.
# @req $COLLECTION_LABELS: Mapping between collections and lables (see configuration)
# @req $UUIDGEN: `uuidgen` command
# @req $ROOT: Path that contains the collections (see configuration)
# @req $EDITOR: Environment variable of your favorite editor
# @req $AWK_GET: Awk script to extract fields from iCalendar file
# @req $AWK_new: Awk script to generate iCalendar file
__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 -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`
# @req $ROOT: Path that contains the collections (see configuration)
# @req $AWK_GET: Awk script to extract fields from iCalendar file
__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 -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
# @req $ROOT: Path that contains the collections (see configuration)
# @req $UUIDGEN: `uuidgen` command
# @req $AWK_SET: Awk script to set field value
__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 -m "Imported event '$(__summary_for_commit "$fpath") ...'" -- "$fpath"
fi
}
# Set status of appointment to CANCELLED or CONFIRMED (toggle)
#
# @input $1: path to iCalendar file
# @req $ROOT: Path that contains the collections (see configuration)
# @req $AWK_SET: Awk script to set field value
# @req $AWK_GET: Awk script to extract fields from 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 -m "Event '$(__summary_for_commit "$fpath") ...' has now status $status" -- "$fpath"
fi
}
# Toggle status flag: CONFIRMED <-> TENTATIVE
#
# @input $1: path to iCalendar file
# @req $ROOT: Path that contains the collections (see configuration)
# @req $AWK_SET: Awk script to set field value
# @req $AWK_GET: Awk script to extract fields from 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 -m "Event '$(__summary_for_commit "$fpath") ...' has now status $status" -- "$fpath"
fi
}
# Prepend attachment to iCalendar file
#
# @input $1: path to iCalendar file
# @req $ROOT: Path that contains the collections (see configuration)
# @req $FZF: Fuzzy finder
# @req $AWK_ATTACH: Awk script to add attachment
__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 -m "Added attachment to '$(__summary_for_commit "$fpath") ...'" -- "$fpath"
fi
rm "$fenc"
}

41
src/sh/load.sh Normal file
View File

@ -0,0 +1,41 @@
# 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" \
"$AWK_APPROX"
}
# For every relevant week, print associated iCalendar files
__load_weeks() {
dates=$(awk -F'|' '{ 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 [ -n "${APPROX_DATA_FILE:-}" ]; then
rm -f "$APPROX_DATA_FILE"
fi
if [ -n "${WEEKLY_DATA_FILE:-}" ]; then
rm -f "$WEEKLY_DATA_FILE"
fi
APPROX_DATA_FILE=$(mktemp)
__load_approx_data >"$APPROX_DATA_FILE"
WEEKLY_DATA_FILE=$(mktemp)
__load_weeks >"$WEEKLY_DATA_FILE"
trap 'rm -f "$APPROX_DATA_FILE" "$WEEKLY_DATA_FILE"' EXIT INT
}

27
src/sh/misc.sh Normal file
View File

@ -0,0 +1,27 @@
# err()
# This is a helper function to print errors.
#
# @input $1: Error message
err() {
echo "$1" >/dev/tty
}
# 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
}

51
src/sh/preview.sh Normal file
View 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"
}

8
src/sh/theme.sh Normal file
View File

@ -0,0 +1,8 @@
GREEN="\033[1;32m"
RED="\033[1;31m"
WHITE="\033[1;97m"
CYAN="\033[1;36m"
STRIKE="\033[9m"
ITALIC="\033[3m"
FAINT="\033[2m"
OFF="\033[m"

130
src/sh/view.sh Normal file
View File

@ -0,0 +1,130 @@
# View Functions
# - __view_day
# - __view_week
# - __view_all
# This function prints the view for the day specified in `$DISPLAY_DATE`.
__view_day() {
weeknr=$(date -d "$DISPLAY_DATE" +"%G.%V")
files=$(grep "^$weeknr\ " "$WEEKLY_DATA_FILE" | cut -d " " -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
})
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="$(echo "$1" | sed 's/|/ /g')" # we will use | as delimiter (need to convert back!)
shift
collection="$1"
shift
status="$1"
shift
description="$(echo "$*" | sed 's/|/:/g')" # we will use | as delimiter
#
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
echo "$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" "$AWK_DAYVIEW"
}
# This function prints the view for the week that contains the day specified in `$DISPLAY_DATE`.
__view_week() {
weeknr=$(date -d "$DISPLAY_DATE" +"%G.%V")
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=$({
set -- $files
for file in "$@"; do
file="$ROOT/$file"
awk \
-v collection_labels="$COLLECTION_LABELS" \
"$AWK_PARSE" "$file"
done
})
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="$FAINT$CYAN"
elif [ "$status" = "CANCELLED" ]; then
symb="$STRIKE"
else
symb=""
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")
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)
fi
sef=$({
echo "$sef"
seq 0 7
} | sort -n)
echo "$sef" | awk -v startofweek="$startofweek" "$AWK_WEEKVIEW"
}
# This function prints all entries.
__view_all() {
cat "$APPROX_DATA_FILE"
}