294 lines
10 KiB
Bash
Executable File
294 lines
10 KiB
Bash
Executable File
#!/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="<span color='#FFB74D'><b>"
|
|
highlight_close="</b></span>"
|
|
|
|
tab=$(printf '\t')
|
|
|
|
die() {
|
|
echo "prayertimes: $1" >&2
|
|
exit 1
|
|
}
|
|
|
|
# "<id><TAB><atoll island>" 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 <<EOF
|
|
$(date -d "$1 00:00" '+%-j %Y %s')
|
|
EOF
|
|
# The tables are 366 rows and always carry a 29 February, so in a common
|
|
# year every day from 1 March on sits one row below its day number.
|
|
if [ "$(date -d "$_year-12-31" +%-j)" -eq 365 ] && [ "$ctx_row" -ge 60 ]; then
|
|
ctx_row=$((ctx_row + 1))
|
|
fi
|
|
}
|
|
|
|
# "<id><TAB><island><TAB><prayer><TAB><HH:MM><TAB><epoch><TAB><scope>" 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
|
|
}
|
|
# <tt> 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\":\"<tt>%s</tt>\",\"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
|