#!/usr/bin/env bash # rice-cal - data and actions for the calendar panel. Events live in khal, # synced with the CalDAV server by vdirsyncer; reminders come from rice-reminder. # # rice-cal month month grid JSON (ISO weeks, Monday first); marks days with events # rice-cal agenda events and reminders for the selected day # rice-cal select pick a day # rice-cal shift <-1|1> previous / next month # rice-cal today # rice-cal add new event on the selected day, typed in rofi: # "15:00 dentist" "15:00-16:30 review" "holiday" (all day) # timed events remind you 10 minutes before; end with !30m or !no # rice-cal sync sync with the server in the background # rice-cal setup connect a CalDAV server (asks for URL, user, password; run in a terminal) set -uo pipefail e() { eww -c "$HOME/.config/eww" "$@"; } var() { e get "$1" 2>/dev/null; } have_khal() { command -v khal >/dev/null && [[ -f ${XDG_CONFIG_HOME:-$HOME/.config}/khal/config ]]; } isodate() { [[ ${1:-} =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] && date -d "$1" +%F >/dev/null 2>&1; } selected() { local d; d=$(var cal_day); isodate "$d" && echo "$d" || date +%F; } offset() { local o; o=$(var cal_offset); [[ $o =~ ^-?[0-9]+$ ]] && echo "$o" || echo 0; } reminders() { rice-reminder json 2>/dev/null || echo '[]'; } month() { local off="${1:-$(offset)}" sel="${2:-$(selected)}" first start end counts="{}" first=$(date -d "$(date +%Y-%m-01) $off month" +%F) start=$(date -d "$first -$(( $(date -d "$first" +%u) - 1 )) days" +%F) end=$(date -d "$start +42 days" +%F) if have_khal; then counts=$(khal list --once --day-format "" --format "{start-date}" "$start" "$end" 2>/dev/null | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' | sort | uniq -c | jq -R -s -c 'split("\n") | map(select(length > 0) | ltrimstr(" ") | capture("^ *(?[0-9]+) (?.+)$") | {(.d): (.n | tonumber)}) | add // {}') fi for i in $(seq 0 41); do date -d "$start +$i days" '+%F %-d %m %u %V'; done | jq -R -s -c --arg title "$(date -d "$first" '+%B %Y')" --arg today "$(date +%F)" --arg sel "$sel" \ --arg month "$(date -d "$first" +%m)" --argjson counts "$counts" --argjson rem "$(reminders)" ' ($rem | group_by(.date) | map({(.[0].date): length}) | add // {}) as $rc | split("\n") | map(select(length > 0) | split(" ")) as $days | { title: $title, dow: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], weeks: [ range(0; 6) as $w | $days[$w * 7:($w + 1) * 7] | { wk: (.[0][4] | tonumber | tostring), days: map({ date: .[0], d: .[1], other: (.[2] != $month), weekend: ((.[3] | tonumber) >= 6), today: (.[0] == $today), selected: (.[0] == $sel and .[0] != $today), events: (($counts[.[0]] // 0) + ($rc[.[0]] // 0)) }) } ] }' } agenda() { local day="${1:-$(selected)}" heading items="[]" status="" sep=$'\x1f' heading=$(date -d "$day" '+%A %-d %B') [[ $day == "$(date +%F)" ]] && heading="Today · $heading" if have_khal; then items=$(khal list --day-format "" --format "{start-time}${sep}{end-time}${sep}{title}${sep}{location}" "$day" 1d 2>/dev/null | jq -R -s -c --arg sep "$sep" ' split("\n") | map(select(length > 0) | split($sep) | { kind: "event", id: "", time: (if .[0] == "" then "all day" else .[0] end), title: .[2], sub: ([(if .[0] != "" and .[1] != "" then "until " + .[1] else empty end), (if (.[3] // "") != "" then .[3] else empty end)] | join(" · ")) })') else status="Events need khal set up (run install.sh)" fi jq -cn --arg heading "$heading" --arg status "$status" --arg day "$day" \ --argjson items "${items:-[]}" --argjson rem "$(reminders)" ' {heading: $heading, status: $status, items: ($items + [ $rem[] | select(.date == $day) | {kind: "reminder", id: .id, time: .time, title: .text, sub: "reminder"} ] | sort_by(if .time == "all day" then "00:00" else .time end))}' } # Push new data straight into eww so clicks feel instant. refresh() { local off="$1" day="$2" e update cal_offset="$off" cal_day="$day" e update month="$(month "$off" "$day")" agenda="$(agenda "$day")" } add() { have_khal || { rice-notify send "Calendar" "Events need khal set up (run install.sh)"; exit 1; } local day input remind=10m start="" end="" title day=$(selected) input=$(rofi -dmenu -theme "$HOME/.config/rofi/prompt.rasi" -p "$(date -d "$day" '+%a %-d %b')" \ -mesg "15:00 dentist · 15:00-16:30 review · holiday · end with !30m or !no") || exit 0 [[ -n $input ]] || exit 0 if [[ $input =~ ^(.*[^[:space:]])[[:space:]]+!(no|[0-9]+[mhd])$ ]]; then input="${BASH_REMATCH[1]}"; remind="${BASH_REMATCH[2]}" fi if [[ $input =~ ^([0-9]{1,2}:[0-9]{2})(-([0-9]{1,2}:[0-9]{2}))?[[:space:]]+(.+)$ ]]; then start="${BASH_REMATCH[1]}"; end="${BASH_REMATCH[3]}"; title="${BASH_REMATCH[4]}" khal new "$day" "$start" "${end:-1h}" "$title" >/dev/null 2>&1 || { rice-notify send "Calendar" "khal could not add that"; exit 1; } if [[ $remind != no ]]; then local amount="${remind%?}" unit case "${remind: -1}" in m) unit=minutes ;; h) unit=hours ;; d) unit=days ;; esac at=$(date -d "$day $start $amount $unit ago" '+%F %H:%M' 2>/dev/null) && rice-reminder add "$at" "$title at $start" >/dev/null 2>&1 fi else title="$input" khal new "$day" "$title" >/dev/null 2>&1 || { rice-notify send "Calendar" "khal could not add that"; exit 1; } fi rice-notify send "Event added" "$title · $(date -d "$day" '+%a %-d %b') ${start:-all day}" setsid -f vdirsyncer sync >/dev/null 2>&1 refresh "$(offset)" "$day" } setup() { local conf="${XDG_CONFIG_HOME:-$HOME/.config}" share="${XDG_DATA_HOME:-$HOME/.local/share}" local secrets="$conf/rice/secrets" data="$share/calendars" url user pass first command -v vdirsyncer >/dev/null && command -v khal >/dev/null || { echo "rice-cal: install khal and vdirsyncer first"; return 1; } read -rp "CalDAV URL [https://dav.srtk.in/dav.php/]: " url "$secrets/caldav") unset pass mkdir -p "$conf/vdirsyncer" "$conf/khal" "$data" "$share/vdirsyncer/status" cat > "$conf/vdirsyncer/config" </dev/null || { echo "rice-cal: could not sign in to $url with those details"; return 1; } vdirsyncer metasync >/dev/null 2>&1 vdirsyncer sync || { echo "rice-cal: first sync failed"; return 1; } first=$(find "$data" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort | head -n1) [[ -n $first ]] || { first=personal; mkdir -p "$data/$first"; } cat > "$conf/khal/config" </dev/null 2>&1 echo "Calendar ready: new events go to '$first', and it syncs every 15 minutes." } case "${1:-}" in setup) setup ;; month) month ;; agenda) agenda ;; select) isodate "${2:-}" || { echo "rice-cal: select takes YYYY-MM-DD" >&2; exit 2; } now=$(date +%Y*12+%-m | sed 's/^0*//'); then=$(date -d "$2" +%Y*12+%-m) refresh "$(( then - now ))" "$2" ;; shift) [[ ${2:-} =~ ^-?[0-9]+$ ]] || { echo "rice-cal: shift takes a number" >&2; exit 2; } off=$(( $(offset) + $2 )) if (( off == 0 )); then day=$(date +%F); else day=$(date -d "$(date +%Y-%m-01) $off month" +%F); fi refresh "$off" "$day" ;; today) refresh 0 "$(date +%F)" ;; add) add ;; sync) setsid -f vdirsyncer sync >/dev/null 2>&1 ;; *) sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;; esac