The bare $HOME repo is gone. This is now the rice: install.sh, rice-* scripts in bin/, configs in home/ linked into $HOME, theme templates, st and the particle wallpaper. Clone to ~/rice and run install.sh.
219 lines
8.9 KiB
Bash
Executable file
219 lines
8.9 KiB
Bash
Executable file
#!/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 <YYYY-MM-DD> 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("^ *(?<n>[0-9]+) (?<d>.+)$") | {(.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 </dev/tty
|
|
url="${url:-https://dav.srtk.in/dav.php/}"
|
|
[[ $url =~ ^https://[^\"[:space:]]+$ ]] || { echo "rice-cal: use an https:// URL"; return 1; }
|
|
read -rp "Username: " user </dev/tty
|
|
[[ $user =~ ^[A-Za-z0-9._@+-]+$ ]] || { echo "rice-cal: that username has characters vdirsyncer can't take"; return 1; }
|
|
read -rsp "Password: " pass </dev/tty; echo
|
|
|
|
# The password stays in a private file; the configs only point at it.
|
|
mkdir -p "$secrets" && chmod 700 "$secrets"
|
|
(umask 077; printf '%s' "$pass" > "$secrets/caldav")
|
|
unset pass
|
|
mkdir -p "$conf/vdirsyncer" "$conf/khal" "$data" "$share/vdirsyncer/status"
|
|
|
|
cat > "$conf/vdirsyncer/config" <<EOF
|
|
# Written by rice-cal setup. The password is read from $secrets/caldav.
|
|
[general]
|
|
status_path = "$share/vdirsyncer/status/"
|
|
|
|
[pair calendar]
|
|
a = "calendar_local"
|
|
b = "calendar_remote"
|
|
collections = ["from a", "from b"]
|
|
metadata = ["displayname", "color"]
|
|
conflict_resolution = "b wins"
|
|
|
|
[storage calendar_local]
|
|
type = "filesystem"
|
|
path = "$data/"
|
|
fileext = ".ics"
|
|
|
|
[storage calendar_remote]
|
|
type = "caldav"
|
|
url = "$url"
|
|
username = "$user"
|
|
password.fetch = ["command", "cat", "$secrets/caldav"]
|
|
EOF
|
|
|
|
echo "Connecting to $url ..."
|
|
yes | vdirsyncer discover calendar >/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" <<EOF
|
|
# Written by rice-cal setup.
|
|
[calendars]
|
|
[[synced]]
|
|
path = $data/*
|
|
type = discover
|
|
|
|
[locale]
|
|
timeformat = %H:%M
|
|
dateformat = %Y-%m-%d
|
|
longdateformat = %Y-%m-%d
|
|
datetimeformat = %Y-%m-%d %H:%M
|
|
longdatetimeformat = %Y-%m-%d %H:%M
|
|
firstweekday = 0
|
|
|
|
[default]
|
|
default_calendar = $first
|
|
highlight_event_days = True
|
|
EOF
|
|
|
|
systemctl --user enable --now vdirsyncer.timer >/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
|