Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ require("lualine").setup({
| `country` | `"Indonesia"` | Must be a non-empty string. |
| `method` | `2` | Numeric method ID per Aladhan’s API. Non-numeric values are ignored with a warning. |
| `duha_offset_minutes` | `15` | Minutes after sunrise before Duha begins (0-180). |
| `prayers` | `{ Fajr = true, Dhuhr = true, Asr = true, Maghrib = true, Isha = true }` | Table of booleans that decide which prayers should fire the `PrayertimeAdhan` event/audio. Override individual keys, e.g. `{ Fajr = true, Isha = false }`. |

Invalid values fall back to the defaults and emit a `vim.notify` warning so mistakes are obvious.

Expand Down Expand Up @@ -187,13 +188,21 @@ and `ev.data.time`, making it easy to wire extra alerts:
vim.api.nvim_create_autocmd("User", {
pattern = "PrayertimeAdhan",
callback = function(ev)
vim.notify(("Time for %s (%s)"):format(ev.data.prayer, ev.data.time))
if ev.data.prayers and ev.data.prayers[ev.data.prayer] then
vim.notify(("Time for %s (%s)"):format(ev.data.prayer, ev.data.time))
end
end,
})
```

Use this hook for chimes, integration scripts, or analytics.

Only one Neovim session emits the event/audio. A lightweight lock file under
`stdpath("state")` elects a "leader" instance, preventing five running Neovims
from blasting five adhans simultaneously. If the lock cannot be acquired (for
example, due to a read-only filesystem) the plugin logs a warning and still
fires the event so you never miss a reminder.

---

Need to test your handler quickly? Run `:PrayerTest` (optionally pass a name and HH:MM time) and your configured autocmd will execute immediately without waiting for the real schedule.
Expand All @@ -204,7 +213,7 @@ Example: play a custom WAV adhan with `aplay`, plus a terminal bell and notifica
vim.api.nvim_create_autocmd("User", {
pattern = "PrayertimeAdhan",
callback = function(ev)
if ev.data.prayer ~= "Sunrise" then
if ev.data.prayers and ev.data.prayers[ev.data.prayer] then
vim.api.nvim_out_write("\7") -- terminal bell
vim.fn.jobstart({ "aplay", "/home/user/Music/adhan.wav" }, { detach = true })
end
Expand All @@ -220,6 +229,28 @@ Available fields inside the autocmd callback:
| --- | --- | --- |
| `ev.data.prayer` | `string` | Name of the prayer that just started (`"Fajr"`, `"Dhuhr"`, etc.). |
| `ev.data.time` | `string` | Scheduled HH:MM timestamp for that prayer. |
| `ev.data.prayers` | `table<string, boolean>` | Copy of the configured `prayers` table so handlers can quickly check `if ev.data.prayers[ev.data.prayer] then ... end`. |

## 🔧 Customizing prayers

Only want a handful of prayers to trigger adhans/automation? Override the `prayers`
table during setup:

```lua
require("prayertime").setup({
city = "Jakarta",
prayers = {
Fajr = true,
Dhuhr = true,
Asr = false, -- skip Asr audio
Maghrib = true,
Isha = false, -- silent Isha
},
})
```

Any omitted key defaults to `true` for the five daily prayers. The active table is
passed to handlers via `ev.data.prayers`, so your autocmds can make the same checks.

## 🪟 Quick display

Expand Down
30 changes: 29 additions & 1 deletion doc/prayertime.txt
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,31 @@ to the defaults and trigger |vim.notify()| warnings so mistakes are obvious:
country string "Indonesia" non-empty string.
method number 2 (Aladhan API method id, see below).
duha_offset_minutes number 15 (0-180 minutes after sunrise for Duha).
prayers table { Fajr=true, Dhuhr=true, Asr=true, Maghrib=true,
Isha=true } boolean map that decides which
prayers trigger |PrayertimeAdhan|.

Method identifiers follow https://aladhan.com/prayer-times-api (e.g. 2 for
ISNA, 20 for KEMENAG). If omitted, Aladhan infers the closest authority based
on the provided city/country/coordinates, but explicitly setting the method
keeps results deterministic.

Customize the `prayers` map to silence specific windows:
>
require("prayertime").setup({
city = "Jakarta",
prayers = {
Fajr = true,
Dhuhr = true,
Asr = false, -- skip Asr audio
Maghrib = true,
Isha = false, -- no Isha reminder
},
})
<
Any key you omit falls back to `true`. The final map is available in
|PrayertimeAdhan| handlers via `ev.data.prayers`.

==============================================================================
CACHE & RELIABILITY *prayertime.nvim-cache*

Expand Down Expand Up @@ -152,11 +171,18 @@ and `ev.data.time` (HH:MM). Example:
vim.api.nvim_create_autocmd("User", {
pattern = "PrayertimeAdhan",
callback = function(ev)
vim.notify(("Time for %s (%s)"):format(ev.data.prayer, ev.data.time))
if ev.data.prayers and ev.data.prayers[ev.data.prayer] then
vim.notify(("Time for %s (%s)"):format(ev.data.prayer, ev.data.time))
end
end,
})
<
Use this hook to trigger custom audio, lighting, or integration scripts.
Internally a lightweight lock file under |stdpath("state")| elects a single
leader Neovim instance, so if you have multiple editors open only one of them
plays the adhan. When the lock cannot be created (e.g. read-only filesystem)
the plugin logs a warning and still fires your autocmd so reminders are never
missed.

For quick testing without waiting for the real timer, run `:PrayerTest`. You can
optionally pass a prayer name and HH:MM string: `:PrayerTest Fajr 05:00`.
Expand All @@ -165,6 +191,8 @@ Callback fields:

• `ev.data.prayer` — string with the prayer name (Fajr, Dhuhr, etc.).
• `ev.data.time` — string HH:MM timestamp for that prayer.
• `ev.data.prayers` — table<string,boolean> copy of your |prayers| option for
quick checks such as `if ev.data.prayers.Fajr then …`.

==============================================================================
FORMATS AND EXTENSIBILITY *prayertime.nvim-formats*
Expand Down
78 changes: 70 additions & 8 deletions lua/prayertime/formats/standard.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ local defaults = {
country = "Indonesia",
method = 2,
duha_offset_minutes = 15,
prayers = {
Fajr = true,
Dhuhr = true,
Asr = true,
Maghrib = true,
Isha = true,
},
}

M.defaults = vim.deepcopy(defaults)
Expand Down Expand Up @@ -42,6 +49,15 @@ local cache_file = cache_dir .. "/schedule.json"
local MAX_FETCH_ATTEMPTS = 3
local RETRY_DELAY_MS = 1000
local load_cache
local lock = require("prayertime.lock")
local state_dir = vim.fn.stdpath("state")
local lock_path = state_dir .. "/prayertime.lock"
local lock_ttl = 6 -- seconds
local lock_warning_emitted = false

pcall(vim.fn.mkdir, state_dir, "p")
-- on lock init
lock.start_heartbeat({ lock_path = lock_path, ttl = lock_ttl })

local function clone_table(value)
if value == nil then
Expand All @@ -50,6 +66,33 @@ local function clone_table(value)
return vim.tbl_deep_extend("force", {}, value)
end

local function merge_prayers(base, overrides)
if type(base) ~= "table" then
base = {}
end
if type(overrides) ~= "table" then
return base
end
for name, enabled in pairs(overrides) do
if type(name) == "string" then
base[name] = not not enabled
end
end
return base
end

local function prayers_signature(prayers)
if type(prayers) ~= "table" then
return ""
end
local pieces = {}
for name, enabled in pairs(prayers) do
table.insert(pieces, ("%s=%s"):format(name, enabled and "1" or "0"))
end
table.sort(pieces)
return table.concat(pieces, ",")
end

local function config_signature(cfg)
if type(cfg) ~= "table" then
return ""
Expand All @@ -59,6 +102,7 @@ local function config_signature(cfg)
cfg.country or defaults.country,
tostring(cfg.method or defaults.method),
tostring(cfg.duha_offset_minutes or defaults.duha_offset_minutes),
prayers_signature(cfg.prayers or defaults.prayers),
}, "::")
end

Expand Down Expand Up @@ -98,11 +142,27 @@ local function warn(msg)
end

local function emit_adhan_event(name, time)
local ok, leader = pcall(lock.try_acquire, { lock_path = lock_path, ttl = lock_ttl })
if not ok then
if not lock_warning_emitted then
warn("prayertime: shared lock unavailable; duplicate adhans may occur")
lock_warning_emitted = true
end
leader = true
end

-- Only leader emits the user event (so only one session triggers user's autocmd)
if not leader then
return
end

local prayers = config.prayers or defaults.prayers or {}

vim.schedule(function()
pcall(vim.api.nvim_exec_autocmds, "User", {
pattern = "PrayertimeAdhan",
modeline = false,
data = { prayer = name, time = time },
data = { prayer = name, time = time, prayers = prayers or {} },
})
end)
end
Expand Down Expand Up @@ -155,6 +215,8 @@ local function apply_config(opts)
warn("prayertime: duha_offset_minutes must be between 0 and 180")
end

new.prayers = merge_prayers(clone_table(defaults.prayers), opts.prayers)

config = new
local new_signature = config_signature(config)
if new_signature ~= previous_signature then
Expand All @@ -178,10 +240,10 @@ local function compute_derived_times()
if sunrise_minutes and dhuhr_minutes and dhuhr_minutes > sunrise_minutes then
local offset = tonumber(config.duha_offset_minutes) or defaults.duha_offset_minutes
offset = math.max(0, offset)
local start_minutes = sunrise_minutes + offset
if start_minutes < dhuhr_minutes then
local duha_start = util.minutes_to_time(start_minutes)
local duha_finish = util.minutes_to_time(dhuhr_minutes)
local start_minutes = sunrise_minutes + offset
if start_minutes < dhuhr_minutes then
local duha_start = util.minutes_to_time(start_minutes)
local duha_finish = util.minutes_to_time(dhuhr_minutes)
derived.Duha = duha_start
derived_ranges.Duha = {
start = duha_start,
Expand Down Expand Up @@ -290,8 +352,8 @@ function M.get_status()
local now_minutes = util.parse_time_str(os.date("%H:%M"))
local duha_range = derived_ranges.Duha
if duha_range and duha_range.start and now_minutes then
local duha_start = util.parse_time_str(duha_range.start)
local duha_end = util.parse_time_str(duha_range.finish or prayer_times.Dhuhr)
local duha_start = util.parse_time_str(duha_range.start)
local duha_end = util.parse_time_str(duha_range.finish or prayer_times.Dhuhr)
if duha_start and now_minutes >= duha_start then
if not duha_end or now_minutes < duha_end then
if duha_range.finish then
Expand All @@ -308,7 +370,7 @@ function M.get_status()
for _, name in ipairs(prayer_order) do
local time = derived_times[name] or prayer_times[name]
if time then
local minutes = util.parse_time_str(time)
local minutes = util.parse_time_str(time)
if minutes and now_minutes and minutes >= now_minutes then
next_name = name
next_time = time
Expand Down
17 changes: 17 additions & 0 deletions lua/prayertime/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,18 @@ local function call_active(method, ...)
return fn(...)
end

local function get_active_prayers()
local ok, cfg = pcall(call_active, "get_config")
if ok and type(cfg) == "table" and type(cfg.prayers) == "table" then
return vim.deepcopy(cfg.prayers) or {}
end
local defaults = active_format and active_format.defaults
if type(defaults) == "table" and type(defaults.prayers) == "table" then
return vim.deepcopy(defaults.prayers) or {}
end
return {}
end

local function close_today_display()
if today_display.win and vim.api.nvim_win_is_valid(today_display.win) then
vim.api.nvim_win_close(today_display.win, true)
Expand Down Expand Up @@ -452,7 +464,12 @@ vim.api.nvim_create_user_command("PrayerTest", function(params)
local payload = {
prayer = (pieces[1] and pieces[1] ~= "" and pieces[1]) or "Test",
time = (pieces[2] and pieces[2] ~= "" and pieces[2]) or os.date("%H:%M"),
prayers = get_active_prayers(),
}

if type(payload.prayers) ~= "table" then
payload.prayers = { Test = true }
end
local ok, err = pcall(vim.api.nvim_exec_autocmds, "User", {
pattern = "PrayertimeAdhan",
modeline = false,
Expand Down
Loading