#!/bin/sh # prayertimes -- next prayer time, and desktop notifications for it. # # Lookup logic follows git.shihaam.dev/flamexode/prayertimes-sh; the tables are # the Salat MV export from github.com/WovenCoast/prayertimes-db. # # Usage: prayertimes.sh [next|today|notify|waybar] # # Which islands to report comes from islands.conf; every island's table is # installed locally, so switching is an edit and a rebuild, never a download. # # POSIX sh. `date -d` is GNU; there is no portable way to parse a date. set -e -u conf="${PRAYERTIMES_CONF:-${XDG_CONFIG_HOME:-$HOME/.config}/prayertimes/islands.conf}" data="${PRAYERTIMES_DATA:-${XDG_DATA_HOME:-$HOME/.local/share}/prayertimes}" state_file="${XDG_STATE_HOME:-$HOME/.local/state}/prayertimes/notified" warn_minutes="${PRAYERTIMES_WARN_MINUTES:-10}" # How late a missed tick may still announce a prayer, so that suspending across # one and resuming later stays quiet rather than shouting about the past. grace_seconds=300 highlight_open="" highlight_close="" tab=$(printf '\t') die() { echo "prayertimes: $1" >&2 exit 1 } # "" for each uncommented line. islands() { awk ' /^[[:space:]]*#/ || NF < 2 || $1 !~ /^[0-9]+$/ { next } { name = $2 for (i = 3; i <= NF; i++) name = name " " $i printf "%s\t%s\n", $1, name } ' "$conf" } # Sets ctx_row and ctx_midnight for a day `date -d` understands. day_context() { read -r ctx_row _year ctx_midnight <" for every # active island, today then tomorrow. records() { day_context today _row_today=$ctx_row _mid_today=$ctx_midnight day_context tomorrow _row_tomorrow=$ctx_row _mid_tomorrow=$ctx_midnight islands | while IFS="$tab" read -r id name; do prayers "$id" "$name" "$_row_today" "$_mid_today" today prayers "$id" "$name" "$_row_tomorrow" "$_mid_tomorrow" tomorrow done } prayers() { # Columns 1 and 2 are the day and month labels, which are a day out # upstream and unused here; column 4 is sunrise, which is not a prayer. # Prayer times are wall clock and Maldives has no DST, so midnight plus an # offset is exact. awk -F, -v id="$1" -v name="$2" -v row="$3" -v midnight="$4" -v scope="$5" ' NR == row { split("Fajr Dhuhr Asr Maghrib Isha", prayer, " ") split("3 5 6 7 8", column, " ") for (i = 1; i <= 5; i++) { split($(column[i]), hm, ":") printf "%s\t%s\t%s\t%02d:%02d\t%d\t%s\n", id, name, prayer[i], \ hm[1], hm[2], midnight + (hm[1] * 60 + hm[2]) * 60, scope } exit } ' "$data/$1.csv" } # Umm al-Qura, as a month-length table: one character per month from 1 Muharram # 1440 (JDN 2458373), 1 for a 30 day month and 0 for a 29 day one. The usual # arithmetic Hijri calendars sit a day off Umm al-Qura about half the time, # which a 720 byte table avoids outright. Runs out after 1499 AH. hijri_for() { date -d "$1" '+%Y %-m %-d' | awk -v months="$(hijri_months)" ' BEGIN { split("Muharram|Safar|Rabi al-Awwal|Rabi al-Thani|Jumada al-Ula|" \ "Jumada al-Akhirah|Rajab|Shaban|Ramadan|Shawwal|" \ "Dhul Qadah|Dhul Hijjah", name, "|") } { # Gregorian to Julian day number. int() truncates toward zero, # which is what this formula wants. a = int(($2 - 14) / 12) jdn = int((1461 * ($1 + 4800 + a)) / 4) \ + int((367 * ($2 - 2 - 12 * a)) / 12) \ - int((3 * int(($1 + 4900 + a) / 100)) / 4) + $3 - 32075 left = jdn - 2458373 if (left < 0) exit for (i = 1; i <= length(months); i++) { days = substr(months, i, 1) == "1" ? 30 : 29 if (left < days) { printf "%d %s %d\n", left + 1, name[(i - 1) % 12 + 1], \ 1440 + int((i - 1) / 12) exit } left -= days } } ' } hijri_months() { printf '%s' \ "010111010100101011011010010101011010101010101011010110010101" \ "011101001001011101100100101110101010010110110101001010110110" \ "101001010110110100101010111010010101011100101010011101010101" \ "001101011010100101011101010010011011101001001101110100100110" \ "110101010011010110101010101010101101010010110110101001010111" \ "010100100111101010010101101101001010101101010101001101101100" \ "100110101110010010110110101010010110101101001010110110100101" \ "010111010010010111011001001011011100100101101101010010101101" \ "011001010101011011010010101101101001001101110100100110110110" \ "010011010111001010101011010101001011011010100101011101010010" \ "101101101001010101101011001010101101100101001101110010010101" \ "110101001010111010100101011011001010101011010101010101010110" } cmd_next() { records | awk -F"$tab" -v now="$(date +%s)" -v multi="$(islands | wc -l)" ' $5 >= now && !seen[$1]++ { printf("%s%s %s\n", multi > 1 ? $2 ": " : "", $3, $4) } ' } cmd_today() { records | awk -F"$tab" -v multi="$(islands | wc -l)" ' $6 != "today" { next } multi > 1 && !seen[$1]++ { printf("%s%s\n", blocks++ ? "\n" : "", $2) } { printf("%-8s %s\n", $3, $4) } ' } # One JSON object, per waybar's custom module protocol. The tooltip is pango # markup, so the next prayer is highlighted rather than marked with a glyph. cmd_waybar() { records | awk \ -F"$tab" \ -v now="$(date +%s)" \ -v warn="$((warn_minutes * 60))" \ -v hijri="$(hijri_for today)" \ -v hl_open="$highlight_open" \ -v hl_close="$highlight_close" ' function left(seconds, total, h, m) { total = int((seconds + 59) / 60) h = int(total / 60) m = total % 60 return h > 0 ? h "h " m "m" : m "m" } function label(name, p) { p = index(name, " ") return p ? substr(name, p + 1) : name } function push(s, hot, i) { rows++ row[rows] = s warm[rows] = hot if (length(s) > width) width = length(s) } function indent(n, s) { s = "" while (n-- > 0) s = s " " return s } { n++ id[n] = $1; island[n] = $2; prayer[n] = $3 at[n] = $4; epoch[n] = $5; scope[n] = $6 if (!seen[$1]++) order[++islands] = $1 } END { for (k = 1; k <= islands; k++) { upcoming = 0 for (i = 1; i <= n; i++) if (id[i] == order[k] && epoch[i] >= now && !upcoming) upcoming = i if (!upcoming) continue if (rows) push("") push(island[upcoming] " prayer times") if (hijri) push(hijri) push("") for (i = 1; i <= n; i++) { if (id[i] != order[k] || scope[i] != "today") continue push(sprintf("%-8s %5s", prayer[i], at[i]), i == upcoming) } push("") push(prayer[upcoming] " in " left(epoch[upcoming] - now) \ (scope[upcoming] == "tomorrow" ? " (tomorrow)" : "")) text = text (text ? " \302\267 " : "") \ (islands > 1 ? label(island[upcoming]) " " : "") \ prayer[upcoming] " " at[upcoming] if (epoch[upcoming] - now <= warn) soon = 1 } if (!text) { print "{\"text\":\"\",\"tooltip\":\"prayertimes: no island selected\"}" exit } # because the bar font is proportional, which leaves the # columns ragged however they are padded. for (i = 1; i <= rows; i++) { line = row[i] == "" ? "" : indent(int((width - length(row[i])) / 2)) \ (warm[i] ? hl_open row[i] hl_close : row[i]) tip = tip line (i < rows ? "\\n" : "") } printf "{\"text\":\"%s\",\"tooltip\":\"%s\",\"class\":\"%s\"}\n", \ text, tip, soon ? "soon" : "" } ' } # Notifies once per key. The key is only recorded once notify-send has # succeeded, so a tick landing before the notification daemon is up retries # rather than swallowing the prayer. fire() { if grep -q -x -F -e "$1" "$state_file" 2>/dev/null; then return 0 fi notify-send \ --app-name="Prayer Times" \ --icon=alarm-symbolic \ --urgency="$2" \ -- "$3" "$4" || return 0 printf '%s\n' "$1" >>"$state_file" _keep=$(tail -n 40 "$state_file") && printf '%s\n' "$_keep" >"$state_file" } cmd_notify() { mkdir -p "$(dirname "$state_file")" _now=$(date +%s) _multi=$(islands | wc -l) records | while IFS="$tab" read -r id name prayer at epoch scope; do [ "$scope" = today ] || continue [ "$_multi" -gt 1 ] && at="$at - $name" delta=$((epoch - _now)) if [ "$delta" -gt 0 ] && [ "$delta" -le $((warn_minutes * 60)) ]; then # Round up, so the tick at exactly T-10:00 still says 10. fire "warn:$id:$epoch" normal \ "$prayer in $(((delta + 59) / 60)) min" "$at" elif [ "$delta" -le 0 ] && [ "$delta" -gt $((-grace_seconds)) ]; then fire "now:$id:$epoch" critical "It's $prayer" "$at" fi done } [ -f "$conf" ] || die "no island config at $conf" [ -d "$data" ] || die "no prayer time tables at $data" [ -n "$(islands)" ] || die "no island uncommented in $conf" for id in $(islands | cut -f1); do [ -s "$data/$id.csv" ] || die "no table for island $id at $data/$id.csv" done case "${1:-next}" in next) cmd_next ;; today) cmd_today ;; notify) cmd_notify ;; waybar) cmd_waybar ;; *) die "usage: prayertimes.sh [next|today|notify|waybar]" ;; esac