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.

<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>
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.
| Mod | What it does | Command | |
|---|---|---|---|
| 🛰️ | mission-control | Live map of agents, tool calls and the code they touch | /mission |
| 📊 | context-bar | Your context window as one stacked bar, a color per category, with token counts and where it compacts | /context-bar |
| 🌦️ | token-weather | Context fill from Clear to Compact soon, plus a prompt-cache countdown | |
| ⏱️ | cache-clock | A 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-i | Goal, doing now, waiting on you, next step | /where |
| ➡️ | next-steps | 2 or 3 likely next prompts after each turn, one key to draft one | 1 2 3, 0 hides |
| 💰 | usage-meter | 5-hour and 7-day plan usage, the reset countdown and the session's cost | |
| 💳 | openai-balance | Your 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-radar | One live line per running subagent | /radar |
| 🔍 | review-watch | One 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-lanes | Whether this session has a browser, and who holds it | /browser |
| 🕌 | prayer-times | The current prayer and how long is left, the next one, and zawal. Computed on your computer, Hanafi or standard Asr | /prayers |
| 🎬 | replay-theater | Steps through the last turn's edits, one diff at a time | /replay |
| 📝 | md-preview | Renders the Markdown files Claude edits like GitHub does, with before and after side by side. Needs Chrome and a terminal that shows images | /md |
| Mod | What it does | Command | |
|---|---|---|---|
| 💥 | blast-radius | Holds rm -r, force pushes and migrations, shows what they'd delete, cancels after 60 s with no answer |
| Mod | What it does | Command | |
|---|---|---|---|
| 🔀 | switchboard | Picks 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 |
These are built around my own tools and rules. Fork them and change the rules to yours.
| Mod | What it does | Command | |
|---|---|---|---|
| 👀 | glance | One line with what needs you: next meeting, PRs, Linear issues, Slack DMs. Needs gh and the Google Calendar, Linear and Slack connectors | /glance |
| 🚦 | merge-gate | Holds 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-guard | Enforces my writing and git rules: rewrites em dashes, asks before --amend, unformatted pushes, emails and phone numbers in notes | |
| 💾 | session-saver | Saves where you left off, shows it on resume. Needs unpause | /park [note] |
| Mod | What it does | Command | |
|---|---|---|---|
| 📱 | reels | YouTube Shorts while Claude works, pauses when it's done | /reels |
| 🐍 | snake | Snake while Claude works | /snake |
| 🎵 | now-playing | What 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>
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.

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, livec shows the code map, with import arrowsThe Code view also needs macOS, Google Chrome and a terminal that shows images (Ghostty, kitty, iTerm2). The Who view works everywhere.
hooks/register.tsx 344 lines1// 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}
344types/index.d.ts 8 lines1export 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