prayer times
This commit is contained in:
Executable
+293
@@ -0,0 +1,293 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user