SLOPSHOPPER

prayer-times

The current prayer and how long is left, the next prayer, and zawal, above the prompt. Computed on your computer from your location; nothing is sent anywhere.

newbandcommandtoasttimer
★ 167v0.1.3MITupdated 2026-10-05hamzafer/claude-code-mods/mods/prayer-times
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · prayer-times
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /prayers ⎿ prayer-times: Set your latitude and longitude in /config (prayer-times). They stay on this computer. 🕌 prayer-times: set your latitude and longitude in /config. They stay on this computer ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
🕌 prayer-times: set your latitude and longitude in /config. They stay on this computer ⟨Claude Code's own drawing⟩
README

<h1 align="center">Claude Code mods</h1>

<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT"></a> <a href="https://github.com/hamzafer/claude-code-mods/actions/workflows/ci.yml"><img src="https://github.com/hamzafer/claude-code-mods/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>

<a href="#-install">Install</a> · <a href="#-the-mods">All mods</a> · <a href="docs/mods.md">Docs</a> · <a href="https://claude.dev/blog/getting-started-with-claude-code-mods/">What are mods?</a>

<table> <tr> <td align="center" width="33%"><a href="docs/mods.md#-context-bar"><img src="images/context-bar.png" alt="context-bar: Claude Code context window usage as a stacked bar, a color per category" width="260"></a><br>📊 <b>context-bar</b><br>what fills your context</td> <td align="center" width="33%"><a href="docs/mods.md#-review-watch"><img src="images/review-watch.png" alt="review-watch: live lines for running Codex and subagent code reviews in Claude Code" width="260"></a><br>🔍 <b>review-watch</b><br>running code reviews, live</td> <td align="center" width="33%"><a href="docs/mods.md#-md-preview"><img src="images/md-preview.png" alt="md-preview: Markdown that Claude Code edits, rendered like GitHub next to the diff" width="260"></a><br>📝 <b>md-preview</b><br>Markdown rendered like GitHub</td> </tr> <tr> <td align="center" width="33%"><a href="docs/mods.md#-blast-radius"><img src="images/gallery/blast-radius.png" alt="blast-radius: a Claude Code hook holds rm -rf and lists the files it would delete" width="260"></a><br>💥 <b>blast-radius</b><br>see what <code>rm -rf</code> would delete</td> <td align="center" width="33%"><a href="docs/mods.md#-now-playing"><img src="images/now-playing.png" alt="now-playing: Spotify track, progress bar and synced lyrics inside Claude Code" width="260"></a><br>🎵 <b>now-playing</b><br>Spotify and its lyrics, live</td> <td align="center" width="33%"><a href="docs/mods.md#-reels-and-snake"><img src="images/reels-demo.gif" alt="reels: YouTube Shorts in a Claude Code pane while it works" width="260"></a><br>📱 <b>reels</b><br>Shorts while Claude works</td> </tr> <tr> <td align="center" width="33%"><a href="docs/mods.md#-where-am-i"><img src="images/gallery/where-am-i.png" alt="where-am-i: the session goal, current step and what waits on you, above the Claude Code prompt" width="260"></a><br>📍 <b>where-am-i</b><br>goal, now, waiting on you</td> <td align="center" width="33%"><a href="docs/mods.md#-lines-above-the-prompt"><img src="images/gallery/lines.png" alt="token-weather, usage-meter and other Claude Code status lines stacked above the prompt" width="260"></a><br>🌦️ <b>token-weather and friends</b><br>lines above the prompt</td> <td align="center" width="33%"><a href="#mission-control"><img src="images/gallery/mission-control.png" alt="mission-control: Claude Code subagents, tool calls and the files they touch, live" width="260"></a><br>🛰️ <b>mission-control</b><br>agents and the code they touch</td> </tr> </table>

🚀 Install

Add the marketplace once, then install any mod by name:

claude plugin marketplace add hamzafer/claude-code-mods
claude plugin install context-bar@claude-code-mods

Or install the general-purpose set in one go:

for m in context-bar token-weather usage-meter where-am-i next-steps agent-radar review-watch replay-theater md-preview blast-radius mission-control; do
  claude plugin install "$m@claude-code-mods"
done

Restart Claude Code after installing. To try one without installing:

git clone https://github.com/hamzafer/claude-code-mods && cd claude-code-mods
claude --plugin-dir mods/context-bar

Needs Claude Code 2.1.287+. A few mods need more (Chrome, gh, a connector); the tables say which.

🧩 The mods

👀 See what's happening

ModWhat it doesCommand
🛰️mission-controlLive map of agents, tool calls and the code they touch/mission
📊context-barYour context window as one stacked bar, a color per category, with token counts and where it compacts/context-bar
🌦️token-weatherContext fill from Clear to Compact soon, plus a prompt-cache countdown
⏱️cache-clockA prompt-cache line under your status line, from Claude Code's own figures: time left, hit rate and misses, and the tokens your next message re-caches once it goes cold. Needs Node and Claude Code 2.1.251+/cache-clock setup
📍where-am-iGoal, doing now, waiting on you, next step/where
➡️next-steps2 or 3 likely next prompts after each turn, one key to draft one1 2 3, 0 hides
💰usage-meter5-hour and 7-day plan usage, the reset countdown and the session's cost
💳openai-balanceYour OpenAI API credit: an estimated balance with a gauge, today's spend, where the money mostly went, and the last call. Needs an OpenAI organization Admin key/openai-balance
📡agent-radarOne live line per running subagent/radar
🔍review-watchOne live line per running code review (Codex or a review subagent) with the model, target, elapsed time and Codex's latest output. A toast lists the findings when it ends
🌐browser-lanesWhether this session has a browser, and who holds it/browser
🕌prayer-timesThe current prayer and how long is left, the next one, and zawal. Computed on your computer, Hanafi or standard Asr/prayers
🎬replay-theaterSteps through the last turn's edits, one diff at a time/replay
📝md-previewRenders the Markdown files Claude edits like GitHub does, with before and after side by side. Needs Chrome and a terminal that shows images/md

🛡️ Guard your repo

ModWhat it doesCommand
💥blast-radiusHolds rm -r, force pushes and migrations, shows what they'd delete, cancels after 60 s with no answer

💸 Spend less

ModWhat it doesCommand
🔀switchboardPicks the model for each subagent that doesn't name one, with OpenAI's Decisions API or Jev, from its short label only. Shows what every subagent cost/route

🔧 My setup (fork and adapt)

These are built around my own tools and rules. Fork them and change the rules to yours.

ModWhat it doesCommand
👀glanceOne line with what needs you: next meeting, PRs, Linear issues, Slack DMs. Needs gh and the Google Calendar, Linear and Slack connectors/glance
🚦merge-gateHolds gh pr merge until CI is green and Codex reviewed once. Needs gh and the Codex CLI. Reviews run on one fixed model; change it to yours/gate
📏rulebook-guardEnforces my writing and git rules: rewrites em dashes, asks before --amend, unformatted pushes, emails and phone numbers in notes
💾session-saverSaves where you left off, shows it on resume. Needs unpause/park [note]

🎮 For fun (opt-in)

ModWhat it doesCommand
📱reelsYouTube Shorts while Claude works, pauses when it's done/reels
🐍snakeSnake while Claude works/snake
🎵now-playingWhat Spotify is playing, with a progress bar, the lyric being sung, and ⏮ ⏸ ⏭ buttons. Needs macOS and the Spotify app/music

<a id="mission-control"></a>

🛰️ Flagship: mission-control

Every subagent, every tool call and every file they touch, in a pane next to the chat. Shown at 4x: two subagents building a logout feature across four files.

mission-control at 4x: two subagents and a logout feature landing across four files

Install it like any mod, restart, and type /mission (or /mission code to open the code map). q closes it.

  • 🤖 w shows the agents and every tool call, live
  • 🗺️ c shows the code map, with import arrows
  • 🔵 Blue while the agent reads a file
  • 🟠 Orange while it writes
  • 🟢 Green when done, with one line on what changed

The Code view also needs macOS, Google Chrome and a terminal that shows images (Ghostty, kitty, iTerm2). The Who view works everywhere.

📚 More

Source 2 files
hooks/register.tsx 344 lines
1// Prayer Times: which prayer is it, and how long is left?
2//   One line above the prompt: the current prayer and the time left to pray it, then the
3//   next one; zawal and the makruh minutes after sunrise and before sunset in red. The times
4//   are computed on this computer from the latitude and longitude in /config (the sun's
5//   position, as prayer apps do it), so the location is never sent anywhere.
6//   /prayers lists today's times. A toast says when a prayer begins, and again when its time
7//   is nearly over.
8import { atom, read, update } from 'claude-code'
9import type { Register } from 'claude-code'
10
11const MIN = 60_000
12const SOON_MIN = 20 // the time left turns yellow under this many minutes
13const LAST_MIN = 5 // and red under this many
14
15// The sun's angle below the horizon for Fajr and Isha; Makkah's Isha is a fixed 90 minutes after Maghrib.
16const METHODS = {
17  karachi: { fajr: 18, isha: 18 },
18  mwl: { fajr: 18, isha: 17 },
19  isna: { fajr: 15, isha: 15 },
20  egypt: { fajr: 19.5, isha: 17.5 },
21  makkah: { fajr: 18.5, isha: 90 / 60, ishaIsMinutes: true },
22} as const
23
24export type Config = {
25  latitude: number
26  longitude: number
27  method: keyof typeof METHODS
28  asr: 'hanafi' | 'standard'
29  highLatitude: 'angle' | 'seventh' | 'middle'
30  adjust?: Partial<Record<Adjustable, number>> // minutes, to match a mosque's timetable
31}
32
33type Adjustable = 'fajr' | 'sunrise' | 'dhuhr' | 'asr' | 'sunset' | 'isha'
34
35// A day's times in hours after midnight UTC of that date (some may fall before 0 or past 24).
36export type Times = { fajr: number; sunrise: number; noon: number; dhuhr: number; asr: number; sunset: number; isha: number; estimated?: boolean }
37
38// Held by the host, so the line keeps ticking across a hot reload.
39const now = atom({ plugin: 'prayer-times', key: 'now' } as const, 0)
40
41export const register: Register = (on, options) => {
42  const cfg = configFrom(options)
43  const makruh = num(options.makruhMinutes, 15)
44  const zawal = num(options.zawalMinutes, 5)
45  const toasts = options.toasts !== false
46  const warnMinutes = Math.min(120, num(options.warnMinutes, 15))
47  let toasted = ''
48  let warned = ''
49
50  on('session.start', async ($, e, next) => {
51    const r = await next(e)
52    await $.command.register({ name: 'prayers', description: "Today's prayer times" }).catch(() => {})
53    const first = await $.clock.now()
54    await update($, now, () => first)
55    // The line moves each minute; a toast marks the start of each prayer, and another its last minutes.
56    $.clock.every(30_000, () => {
57      void (async () => {
58        const t = await $.clock.now()
59        await update($, now, () => t)
60        if (!cfg || !toasts) return
61        const s = status(cfg, new Date(t), makruh, zawal)
62        const key = `${s.current?.name}@${s.current?.start.toDateString()}`
63        if (s.current && t - s.current.start.getTime() < 60_000 && key !== toasted) {
64          toasted = key
65          $.ui.toast(`🕌 ${s.current.name} has begun · until ${hhmm(s.current.end)}`)
66        }
67        const due = deadlineDue(s, new Date(t), warnMinutes, warned, makruh)
68        if (due) {
69          warned = due.key
70          $.ui.toast(due.text)
71        }
72      })().catch(() => {})
73    })
74    return r
75  })
76
77  on('command.run', { command: 'prayers' }, async () => {
78    if (!cfg) return { text: 'Set your latitude and longitude in /config (prayer-times). They stay on this computer.' }
79    const d = new Date()
80    const t = timesFor(cfg, d)
81    const at = (h: number) => hhmm(atHours(d, h))
82    return {
83      text:
84        `Fajr ${at(t.fajr)} · Sunrise ${at(t.sunrise)} · Zawal ${at(t.noon - zawal / 60)} · Dhuhr ${at(t.dhuhr)} · ` +
85        `Asr ${at(t.asr)} · Maghrib ${at(t.sunset)} · Isha ${at(t.isha)} (${cfg.method}, ${cfg.asr} Asr` +
86        `${t.estimated ? ', no sunrise or sunset today: estimated from latitude 65°' : ''})`,
87    }
88  })
89
90  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
91    const rest = await next(e) // what other mods and Claude Code draw here stays
92    if (e.props.hasSurvey) return rest
93    const t = (await read($, now)) || Date.now() // subscribes the line to the clock
94    const { Box, Text } = $.ui.resolve(e)
95    if (!cfg) {
96      return (
97        <Box flexDirection="column">
98          <Box paddingX={1}>
99            <Text dimColor wrap="truncate-end">{'🕌 prayer-times: set your latitude and longitude in /config. They stay on this computer'}</Text>
100          </Box>
101          {rest}
102        </Box>
103      )
104    }
105    const s = status(cfg, new Date(t), makruh, zawal)
106    return (
107      <Box flexDirection="column">
108        <Box paddingX={1}>
109          <Text wrap="truncate-end">
110            {s.parts.map((p, i) => {
111              // A leading emoji is drawn plain: some terminals shift or clip a bold one.
112              const icon = /^(\p{Extended_Pictographic}\uFE0F?) /u.exec(p.text)?.[1]
113              const label = icon ? p.text.slice(icon.length) : p.text
114              return (
115                <Text>
116                  {i > 0 && <Text dimColor>{' · '}</Text>}
117                  {icon && <Text>{icon}</Text>}
118                  <Text color={p.color} bold={p.bold} dimColor={p.dim}>{label}</Text>
119                </Text>
120              )
121            })}
122          </Text>
123        </Box>
124        {rest}
125      </Box>
126    )
127  })
128}
129
130const DHUHR_AFTER = 2 / 60 // Dhuhr starts this long after the sun's highest point
131
132type Part = { text: string; color?: string; bold?: boolean; dim?: boolean }
133type Window = { name: string; start: Date; end: Date }
134
135// What the line says at `at`: the prayer whose time it is and what is left, or the forbidden window, then the next prayer.
136export function status(cfg: Config, at: Date, makruh = 15, zawal = 5) {
137  // Times are UTC hours of each local date, so far from UTC (e.g. UTC+13) a day's prayers can sit two dates back.
138  const windows = [-2, -1, 0, 1].flatMap(offset => dayWindows(cfg, addDays(at, offset), makruh, zawal))
139  const inside = (w: Window) => w.start <= at && at < w.end
140  const prayers = windows.filter(w => !w.name.startsWith('!'))
141  const current = prayers.find(inside)
142  const blocked = windows.find(w => w.name.startsWith('!') && inside(w))
143  const next = prayers.find(w => w.start > at)!
144  const parts: Part[] = []
145
146  if (blocked) {
147    const label = blocked.name === '!zawal' ? 'Zawal' : blocked.name === '!sunrise' ? 'Sunrise' : 'Sunset near'
148    parts.push({ text: `⛔ ${label}`, color: 'red', bold: true })
149    if (blocked.name === '!sunset' && current) parts.push({ text: `${current.name} ${until(current.end, at)} left, makruh now`, color: 'red' })
150    else parts.push({ text: `no prayer for ${until(blocked.end, at)}`, color: 'red' })
151  } else if (current) {
152    const left = (current.end.getTime() - at.getTime()) / MIN
153    parts.push({ text: `🕌 ${current.name}`, bold: true })
154    parts.push({ text: `${until(current.end, at)} left`, color: left < LAST_MIN ? 'red' : left < SOON_MIN ? 'yellow' : 'green', bold: left < SOON_MIN })
155  } else {
156    parts.push({ text: `🕌 ${next.name} in ${until(next.start, at)}`, bold: true })
157  }
158  if (current || blocked) parts.push({ text: `next ${next.name} ${hhmm(next.start)}`, dim: true })
159  // Before Dhuhr, say when zawal comes.
160  const z = windows.find(w => w.name === '!zawal' && w.start > at)
161  if (z && next.name === 'Dhuhr' && !blocked) parts.push({ text: `zawal ${hhmm(z.start)}`, dim: true })
162  if (timesFor(cfg, at).estimated) parts.push({ text: 'estimated (no sunrise or sunset today)', dim: true })
163  return { parts, current, next, blocked }
164}
165
166// The warning that the current prayer's time is nearly over, once per window (`warned` is the last key warned).
167// It also comes when the session starts inside the last minutes, but not for a window no longer than the warning,
168// whose start toast already says when it ends. Asr warns too: its time runs to sunset, through the makruh minutes.
169// Asr's real deadline is the start of the makruh minutes before sunset, so it warns before those.
170export function deadlineDue(s: Pick<ReturnType<typeof status>, 'current'>, at: Date, warnMinutes: number, warned: string, makruh = 0) {
171  const w = s.current
172  if (!w || !(warnMinutes > 0)) return null
173  const key = `${w.name}@${w.start.getTime()}`
174  const end = w.name === 'Asr' && makruh > 0 ? new Date(w.end.getTime() - makruh * MIN) : w.end
175  const left = (end.getTime() - at.getTime()) / MIN
176  const length = (w.end.getTime() - w.start.getTime()) / MIN
177  if (key === warned || left <= 0 || left > warnMinutes || length <= warnMinutes) return null
178  if (end !== w.end) return { key, text: `⏳ Asr: makruh in ${Math.ceil(left)} min (${hhmm(end)}), sunset ${hhmm(w.end)}` }
179  return { key, text: `⏳ ${w.name} ends in ${Math.ceil(left)} min (${hhmm(w.end)})` }
180}
181
182// One day's windows: each prayer from its start to its end (Hanafi), and the times not to pray, marked with `!`.
183function dayWindows(cfg: Config, day: Date, makruh: number, zawal: number): Window[] {
184  const t = timesFor(cfg, day)
185  const tomorrow = timesFor(cfg, addDays(day, 1))
186  const at = (h: number) => atHours(day, h)
187  return [
188    { name: 'Fajr', start: at(t.fajr), end: at(t.sunrise) },
189    { name: '!sunrise', start: at(t.sunrise), end: at(t.sunrise + makruh / 60) },
190    { name: '!zawal', start: at(t.noon - zawal / 60), end: at(t.noon + DHUHR_AFTER) },
191    { name: 'Dhuhr', start: at(t.dhuhr), end: at(t.asr) },
192    { name: 'Asr', start: at(t.asr), end: at(t.sunset) },
193    { name: '!sunset', start: at(t.sunset - makruh / 60), end: at(t.sunset) },
194    { name: 'Maghrib', start: at(t.sunset), end: at(t.isha) },
195    { name: 'Isha', start: at(t.isha), end: at(24 + tomorrow.fajr) },
196  ]
197}
198
199// The day's times at the computer's own time zone for that date.
200// The times for the computer's local date, as UTC hours, so a daylight-saving change that day can't shift them.
201export function timesFor(cfg: Config, day: Date): Times {
202  const t = computeTimes(cfg, day.getFullYear(), day.getMonth() + 1, day.getDate(), 0)
203  for (const [k, m] of Object.entries(cfg.adjust ?? {})) t[k as Adjustable] += m / 60
204  return t
205}
206
207// `asr+2 isha-5` as minutes per time. Dhuhr moves Dhuhr's start only (zawal stays on the sun's highest point); Maghrib moves sunset.
208export function adjustFrom(text: string): Config['adjust'] {
209  const names: Record<string, Adjustable> = { fajr: 'fajr', sunrise: 'sunrise', dhuhr: 'dhuhr', asr: 'asr', maghrib: 'sunset', isha: 'isha' }
210  const out: Partial<Record<Adjustable, number>> = {}
211  for (const m of text.toLowerCase().matchAll(/(fajr|sunrise|dhuhr|asr|maghrib|isha)\s*([+-]?)\s*(\d+(?:\.\d+)?)/g)) out[names[m[1]!]!] = (m[2] === '-' ? -1 : 1) * Number(m[3])
212  return out
213}
214
215// The sun's position for the day, then the times it reaches each prayer's angle (the PrayTimes method).
216export function computeTimes(cfg: Config, year: number, month: number, day: number, zone: number): Times {
217  const t = solve(cfg, year, month, day, zone)
218  if (Number.isFinite(t.sunrise) && Number.isFinite(t.sunset)) return t
219  // Polar day or night: no sunrise or sunset at all. Use the nearest latitude that has them (65°), as many scholars advise.
220  return { ...solve({ ...cfg, latitude: Math.sign(cfg.latitude) * 65 }, year, month, day, zone), estimated: true }
221}
222
223function solve(cfg: Config, year: number, month: number, day: number, zone: number): Times {
224  const { latitude: lat, longitude: lng } = cfg
225  const m = METHODS[cfg.method]
226  const jdate = julian(year, month, day) - lng / (15 * 24)
227  const sun = (t: number) => sunPosition(jdate + t)
228  const midDay = (t: number) => fix(12 - sun(t).equation, 24)
229  const angleTime = (angle: number, t: number, before: boolean) => {
230    const decl = sun(t).declination
231    const noon = midDay(t)
232    const cos = (-sin(angle) - sin(decl) * sin(lat)) / (cos_(decl) * cos_(lat))
233    const span = acos(cos) / 15 // NaN when the sun never reaches the angle
234    return noon + (before ? -span : span)
235  }
236  const asrTime = (factor: number, t: number) => {
237    const decl = sun(t).declination
238    return angleTime(-acot(factor + tan(Math.abs(lat - decl))), t, false)
239  }
240  const factor = cfg.asr === 'hanafi' ? 2 : 1
241
242  // Two passes: the first from rough guesses, the second from the first's answers.
243  let h = { fajr: 5, sunrise: 6, noon: 12, asr: 13, sunset: 18, isha: 18 }
244  for (let pass = 0; pass < 2; pass++) {
245    const d = (x: number) => x / 24
246    h = {
247      fajr: angleTime(m.fajr, d(h.fajr), true),
248      sunrise: angleTime(0.833, d(h.sunrise), true),
249      noon: midDay(d(h.noon)),
250      asr: asrTime(factor, d(h.asr)),
251      sunset: angleTime(0.833, d(h.sunset), false),
252      isha: 'ishaIsMinutes' in m ? NaN : angleTime(m.isha, d(h.isha), false),
253    }
254  }
255  const shift = zone - lng / 15
256  const out: Times = { fajr: h.fajr + shift, sunrise: h.sunrise + shift, noon: h.noon + shift, dhuhr: h.noon + shift + DHUHR_AFTER, asr: h.asr + shift, sunset: h.sunset + shift, isha: h.isha + shift }
257  if ('ishaIsMinutes' in m) out.isha = out.sunset + m.isha
258
259  // Far from the equator the sun may never get low enough: Fajr and Isha take a share of the night instead.
260  const night = 24 - (out.sunset - out.sunrise)
261  const share = (angle: number) => (cfg.highLatitude === 'seventh' ? 1 / 7 : cfg.highLatitude === 'middle' ? 1 / 2 : angle / 60)
262  const fajrMax = share(m.fajr) * night
263  if (!Number.isFinite(out.fajr) || out.sunrise - out.fajr > fajrMax) out.fajr = out.sunrise - fajrMax
264  if (!('ishaIsMinutes' in m)) {
265    const ishaMax = share(m.isha) * night
266    if (!Number.isFinite(out.isha) || out.isha - out.sunset > ishaMax) out.isha = out.sunset + ishaMax
267  }
268  return out
269}
270
271function sunPosition(jd: number) {
272  const D = jd - 2451545
273  const g = fix(357.529 + 0.98560028 * D, 360)
274  const q = fix(280.459 + 0.98564736 * D, 360)
275  const L = fix(q + 1.915 * sin(g) + 0.02 * sin(2 * g), 360)
276  const e = 23.439 - 0.00000036 * D
277  const ra = atan2(cos_(e) * sin(L), cos_(L)) / 15
278  return { declination: asin(sin(e) * sin(L)), equation: q / 15 - fix(ra, 24) }
279}
280
281function julian(year: number, month: number, day: number) {
282  if (month <= 2) {
283    year -= 1
284    month += 12
285  }
286  const a = Math.floor(year / 100)
287  const b = 2 - a + Math.floor(a / 4)
288  return Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + b - 1524.5
289}
290
291const rad = (d: number) => (d * Math.PI) / 180
292const deg = (r: number) => (r * 180) / Math.PI
293const sin = (d: number) => Math.sin(rad(d))
294const cos_ = (d: number) => Math.cos(rad(d))
295const tan = (d: number) => Math.tan(rad(d))
296const asin = (x: number) => deg(Math.asin(x))
297const acos = (x: number) => deg(Math.acos(x))
298const atan2 = (y: number, x: number) => deg(Math.atan2(y, x))
299const acot = (x: number) => deg(Math.atan(1 / x))
300const fix = (a: number, b: number) => {
301  const r = a - b * Math.floor(a / b)
302  return r < 0 ? r + b : r
303}
304
305function configFrom(options: Record<string, unknown>): Config | null {
306  const latitude = Number(options.latitude)
307  const longitude = Number(options.longitude)
308  const isSet = (v: unknown) => v !== undefined && v !== null && v !== ''
309  if (!isSet(options.latitude) || !isSet(options.longitude) || !(Math.abs(latitude) <= 90) || !(Math.abs(longitude) <= 180)) return null
310  const method = String(options.method ?? 'karachi') as Config['method']
311  return {
312    latitude,
313    longitude,
314    method: method in METHODS ? method : 'karachi',
315    asr: options.asr === 'standard' ? 'standard' : 'hanafi',
316    highLatitude: options.highLatitude === 'seventh' || options.highLatitude === 'middle' ? options.highLatitude : 'angle',
317    adjust: adjustFrom(String(options.adjust ?? '')),
318  }
319}
320
321function num(value: unknown, fallback: number) {
322  const n = Number(value)
323  return value === undefined || value === null || value === '' || !Number.isFinite(n) ? fallback : Math.max(0, n)
324}
325
326function addDays(d: Date, n: number) {
327  return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n, 12)
328}
329
330// The instant `hours` after midnight UTC of the day's local date.
331export function atHours(day: Date, hours: number) {
332  return new Date(Date.UTC(day.getFullYear(), day.getMonth(), day.getDate()) + Math.round(hours * 60) * MIN)
333}
334
335export function hhmm(d: Date) {
336  return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
337}
338
339// Time left until `end`, rounded up to the minute: `42m`, `1h 05m`.
340export function until(end: Date, at: Date) {
341  const m = Math.max(0, Math.ceil((end.getTime() - at.getTime()) / MIN))
342  return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, '0')}m`
343}
344
types/index.d.ts 8 lines
1export type Tick = number // the last clock tick, so the line redraws each minute
2
3declare module 'claude-code' {
4  interface PluginState {
5    'prayer-times': { now: Tick }
6  }
7}
8