#!/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