azaan-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/assets/Notification.png +0 -0
- package/assets/icon.png +0 -0
- package/assets/image.png +0 -0
- package/bin/azaan.js +8 -0
- package/package.json +35 -0
- package/src/api.js +38 -0
- package/src/cli.js +161 -0
- package/src/config.js +41 -0
- package/src/daemon.js +71 -0
- package/src/ui.js +122 -0
- package/src/utils.js +90 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Azaan CLI 🕌
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./assets/icon.png" width="150" alt="Azaan CLI Logo" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
A lightweight, beautiful, and interactive CLI to check Azaan times anywhere in the world right from your terminal. Built for humans and status bars.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Install globally via npm:
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g azaan-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
On your first run, the CLI will interactively ask you for your City and Country, and save it in your config.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
azaan
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Override City (One-off):**
|
|
27
|
+
If you want to quickly check another city without changing your configuration:
|
|
28
|
+
```bash
|
|
29
|
+
azaan "London"
|
|
30
|
+
azaan "New York"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Status Bar / Scripts (--status):**
|
|
34
|
+
Output a single-line string of the next prayer and countdown. Perfect for tmux, Waybar, or Polybar.
|
|
35
|
+
```bash
|
|
36
|
+
azaan --status
|
|
37
|
+
# Example Output: Next prayer is Maghrib in 45 mins and 12 seconds at 05:55 PM
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Configure & Reset:**
|
|
41
|
+
To change your default saved city or calculation method later:
|
|
42
|
+
```bash
|
|
43
|
+
azaan config
|
|
44
|
+
```
|
|
45
|
+
To completely clear your local configuration:
|
|
46
|
+
```bash
|
|
47
|
+
azaan reset
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Desktop Notifications (Zero Setup)
|
|
51
|
+
|
|
52
|
+
The `azaan-cli` comes with a built-in lightweight background daemon for notifications. **There is zero setup required.**
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
The moment you run `azaan` in your terminal, the background process starts automatically. It will quietly check the time in the background and send you a native desktop notification **10 minutes before** every Azaan, and exactly **at the time** of the Azaan.
|
|
57
|
+
|
|
58
|
+
### Managing the Daemon
|
|
59
|
+
You don't need to do anything to start it, but if you want manual control, you can use the daemon commands:
|
|
60
|
+
```bash
|
|
61
|
+
# Check if the daemon is running
|
|
62
|
+
azaan daemon status
|
|
63
|
+
|
|
64
|
+
# Stop the background notifications
|
|
65
|
+
azaan daemon stop
|
|
66
|
+
|
|
67
|
+
# Start the daemon manually
|
|
68
|
+
azaan daemon start
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Shoutout 🙌
|
|
72
|
+
A massive thank you and shoutout to [Ahmad Awais](https://github.com/AhmadAwais) for building [ramadan-cli](https://github.com/AhmadAwais/ramadan-cli), which served as the primary inspiration and foundation that led to the creation of Azaan CLI.
|
|
73
|
+
|
|
74
|
+
## Powered By
|
|
75
|
+
- [Aladhan Prayer Times API](https://aladhan.com/prayer-times-api)
|
|
76
|
+
- `commander` & `@clack/prompts`
|
|
Binary file
|
package/assets/icon.png
ADDED
|
Binary file
|
package/assets/image.png
ADDED
|
Binary file
|
package/bin/azaan.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "azaan-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"azaan": "./bin/azaan.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"azaan",
|
|
14
|
+
"prayer",
|
|
15
|
+
"islam",
|
|
16
|
+
"muslim",
|
|
17
|
+
"salah",
|
|
18
|
+
"namaz",
|
|
19
|
+
"adhan",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
"author": "Hamza Rasheed",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"description": "A lightweight, beautiful, and interactive CLI to check Azaan times anywhere in the world.",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@clack/prompts": "^1.0.1",
|
|
27
|
+
"axios": "^1.13.5",
|
|
28
|
+
"chalk": "^5.6.2",
|
|
29
|
+
"cli-table3": "^0.6.5",
|
|
30
|
+
"commander": "^14.0.3",
|
|
31
|
+
"conf": "^15.1.0",
|
|
32
|
+
"date-fns": "^4.1.0",
|
|
33
|
+
"node-notifier": "^10.0.1"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
const ALADHAN_BASE_URL = 'http://api.aladhan.com/v1';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fetches prayer timings for a given date.
|
|
7
|
+
* If date is not provided, defaults to today.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} city
|
|
10
|
+
* @param {string} country
|
|
11
|
+
* @param {number} method - Calculation method
|
|
12
|
+
* @param {string} [date] - DD-MM-YYYY format
|
|
13
|
+
*/
|
|
14
|
+
export async function fetchTimings(city, country, method = 2, date = null) {
|
|
15
|
+
try {
|
|
16
|
+
const endpoint = date
|
|
17
|
+
? `${ALADHAN_BASE_URL}/timingsByCity/${date}`
|
|
18
|
+
: `${ALADHAN_BASE_URL}/timingsByCity`;
|
|
19
|
+
|
|
20
|
+
const params = {
|
|
21
|
+
city,
|
|
22
|
+
country,
|
|
23
|
+
method,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const response = await axios.get(endpoint, { params });
|
|
27
|
+
if (response.data && response.data.code === 200) {
|
|
28
|
+
return response.data;
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error('Invalid response from Aladhan API');
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.response && error.response.data && error.response.data.data) {
|
|
34
|
+
throw new Error(`API Error: ${error.response.data.data}`);
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Failed to fetch timings: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getConfig, setConfig, clearConfig, getDaemonPid, setDaemonPid } from './config.js';
|
|
3
|
+
import { runInteractiveSetup, printTimingsTable, createSpinner } from './ui.js';
|
|
4
|
+
import { fetchTimings } from './api.js';
|
|
5
|
+
import { getNextPrayer, formatCountdown, formatCountdownVerbose, formatTime12h } from './utils.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import notifier from 'node-notifier';
|
|
8
|
+
import { format } from 'date-fns';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
function isDaemonRunning(pid) {
|
|
17
|
+
if (!pid) return false;
|
|
18
|
+
try {
|
|
19
|
+
process.kill(pid, 0);
|
|
20
|
+
return true;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function startDaemonIfNeeded() {
|
|
27
|
+
const pid = getDaemonPid();
|
|
28
|
+
if (isDaemonRunning(pid)) return;
|
|
29
|
+
|
|
30
|
+
const daemonPath = join(__dirname, 'daemon.js');
|
|
31
|
+
const child = spawn(process.execPath, [daemonPath], {
|
|
32
|
+
detached: true,
|
|
33
|
+
stdio: 'ignore'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
child.unref();
|
|
37
|
+
if (child.pid) setDaemonPid(child.pid);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runCli() {
|
|
41
|
+
const program = new Command();
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.name('azaan')
|
|
45
|
+
.description('CLI to check Azaan times anywhere in the world.')
|
|
46
|
+
.version('1.0.0');
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.argument('[city]', 'City to check timings for (one-off)')
|
|
50
|
+
.option('-s, --status', 'Print single-line next prayer status')
|
|
51
|
+
.action(async (cmdCity, options) => {
|
|
52
|
+
let config = getConfig();
|
|
53
|
+
|
|
54
|
+
// If no config and no arguments, ask for setup unless it's a status check
|
|
55
|
+
if (!config.city && !cmdCity && !options.status) {
|
|
56
|
+
config = await runInteractiveSetup({});
|
|
57
|
+
setConfig(config);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const city = cmdCity || config.city;
|
|
61
|
+
const country = config.country || '';
|
|
62
|
+
const method = config.method || 2;
|
|
63
|
+
|
|
64
|
+
if (!city) {
|
|
65
|
+
console.error(chalk.red('City not configured. Run "azaan config" or provide a city parameter.'));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Auto start daemon
|
|
70
|
+
startDaemonIfNeeded();
|
|
71
|
+
|
|
72
|
+
let s;
|
|
73
|
+
if (!options.status) s = createSpinner(`Fetching azaan times for ${city}...`);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const dateStr = format(new Date(), 'dd-MM-yyyy');
|
|
77
|
+
// Actually fetch Timings by City from Aladhan wrapper
|
|
78
|
+
const data = await fetchTimings(city, country, method, dateStr);
|
|
79
|
+
let todayTimings = null;
|
|
80
|
+
|
|
81
|
+
// Find today's data (if array, pick first, usually it's single object for "timingsByCity/date")
|
|
82
|
+
if (data && data.data && data.data.timings) {
|
|
83
|
+
todayTimings = data.data.timings;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (s) s.stop('Times fetched successfully.');
|
|
87
|
+
|
|
88
|
+
if (!todayTimings) {
|
|
89
|
+
throw new Error('Timings not found in response');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (options.status) {
|
|
93
|
+
const next = getNextPrayer(todayTimings);
|
|
94
|
+
if (next) {
|
|
95
|
+
console.log(`Next prayer is ${next.name} in ${formatCountdownVerbose(next.diffSeconds)} at ${formatTime12h(next.time)}`);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
} else {
|
|
98
|
+
console.log('No more prayers today.');
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
printTimingsTable(todayTimings, city);
|
|
104
|
+
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (s) s.stop('Failed to fetch data.', 1);
|
|
107
|
+
console.error(chalk.red('\nError: ' + err.message));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
program
|
|
113
|
+
.command('config')
|
|
114
|
+
.description('Configure default settings')
|
|
115
|
+
.action(async () => {
|
|
116
|
+
const config = getConfig();
|
|
117
|
+
const newConfig = await runInteractiveSetup(config);
|
|
118
|
+
setConfig(newConfig);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
program
|
|
122
|
+
.command('reset')
|
|
123
|
+
.description('Clear saved settings')
|
|
124
|
+
.action(() => {
|
|
125
|
+
clearConfig();
|
|
126
|
+
console.log(chalk.green('Configuration cleared successfully.'));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
program
|
|
130
|
+
.command('daemon [action]')
|
|
131
|
+
.description('Manage the background notification daemon (start/stop/status)')
|
|
132
|
+
.action((action) => {
|
|
133
|
+
const pid = getDaemonPid();
|
|
134
|
+
if (action === 'stop') {
|
|
135
|
+
if (isDaemonRunning(pid)) {
|
|
136
|
+
try {
|
|
137
|
+
process.kill(pid);
|
|
138
|
+
} catch (e) { }
|
|
139
|
+
console.log(chalk.green('Daemon stopped.'));
|
|
140
|
+
} else {
|
|
141
|
+
console.log(chalk.yellow('Daemon is not running.'));
|
|
142
|
+
}
|
|
143
|
+
setDaemonPid(null);
|
|
144
|
+
} else if (action === 'start') {
|
|
145
|
+
if (isDaemonRunning(pid)) {
|
|
146
|
+
console.log(chalk.yellow('Daemon is already running.'));
|
|
147
|
+
} else {
|
|
148
|
+
startDaemonIfNeeded();
|
|
149
|
+
console.log(chalk.green('Daemon started successfully! You will receive notifications in the background.'));
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
if (isDaemonRunning(pid)) {
|
|
153
|
+
console.log(chalk.green(`Daemon is running in the background (PID: ${pid}).`));
|
|
154
|
+
} else {
|
|
155
|
+
console.log(chalk.yellow('Daemon is not running.'));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await program.parseAsync(process.argv);
|
|
161
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
|
|
3
|
+
// Initialize a new config instance specific to azaan-cli
|
|
4
|
+
const config = new Conf({
|
|
5
|
+
projectName: 'azaan-cli',
|
|
6
|
+
defaults: {
|
|
7
|
+
city: null,
|
|
8
|
+
country: null,
|
|
9
|
+
method: 2, // ISNA is method 2 in Aladhan API
|
|
10
|
+
school: 0, // Shafi by default
|
|
11
|
+
daemonPid: null, // Store PID of the detached daemon
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export function getConfig() {
|
|
16
|
+
return {
|
|
17
|
+
city: config.get('city'),
|
|
18
|
+
country: config.get('country'),
|
|
19
|
+
method: config.get('method'),
|
|
20
|
+
school: config.get('school'),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function setConfig(newConfig) {
|
|
25
|
+
if (newConfig.city) config.set('city', newConfig.city);
|
|
26
|
+
if (newConfig.country) config.set('country', newConfig.country);
|
|
27
|
+
if (newConfig.method !== undefined) config.set('method', newConfig.method);
|
|
28
|
+
if (newConfig.school !== undefined) config.set('school', newConfig.school);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getDaemonPid() {
|
|
32
|
+
return config.get('daemonPid');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function setDaemonPid(pid) {
|
|
36
|
+
config.set('daemonPid', pid);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function clearConfig() {
|
|
40
|
+
config.clear();
|
|
41
|
+
}
|
package/src/daemon.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getConfig } from './config.js';
|
|
2
|
+
import { fetchTimings } from './api.js';
|
|
3
|
+
import { getNextPrayer, formatTime12h } from './utils.js';
|
|
4
|
+
import notifier from 'node-notifier';
|
|
5
|
+
import { format } from 'date-fns';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const ICON_PATH = join(__dirname, '../assets/icon.png');
|
|
12
|
+
|
|
13
|
+
// 60 seconds interval
|
|
14
|
+
const INTERVAL_MS = 60 * 1000;
|
|
15
|
+
|
|
16
|
+
// Poll loop
|
|
17
|
+
async function runDaemon() {
|
|
18
|
+
const config = getConfig();
|
|
19
|
+
if (!config.city) return;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const dateStr = format(new Date(), 'dd-MM-yyyy');
|
|
23
|
+
const data = await fetchTimings(config.city, config.country || '', config.method || 2, dateStr);
|
|
24
|
+
const timings = data?.data?.timings;
|
|
25
|
+
|
|
26
|
+
if (!timings) return;
|
|
27
|
+
|
|
28
|
+
const next = getNextPrayer(timings);
|
|
29
|
+
if (next) {
|
|
30
|
+
// Send notification exactly 10 minutes beforehand.
|
|
31
|
+
// Since interval is 60s, diffSeconds will be between 540 and 600 seconds.
|
|
32
|
+
// To ensure we report exactly once, keep track of the reported prayer.
|
|
33
|
+
// Since it's a daemon that could be restarted, checking the time diff is safer.
|
|
34
|
+
const diffMins = next.diffSeconds / 60;
|
|
35
|
+
|
|
36
|
+
// If diff is between 9 and 10 minutes
|
|
37
|
+
if (diffMins > 9 && diffMins <= 10) {
|
|
38
|
+
const minutes = Math.ceil(diffMins);
|
|
39
|
+
|
|
40
|
+
notifier.notify({
|
|
41
|
+
appID: 'Azaan CLI',
|
|
42
|
+
icon: ICON_PATH,
|
|
43
|
+
title: `Azaan Reminder: ${next.name}`,
|
|
44
|
+
message: `${next.name} is starting in ${minutes} minutes at ${formatTime12h(next.time)}!`,
|
|
45
|
+
sound: true,
|
|
46
|
+
wait: false
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If diff is exactly 0 (between 0 and 1 min)
|
|
51
|
+
if (diffMins > 0 && diffMins <= 1) {
|
|
52
|
+
notifier.notify({
|
|
53
|
+
appID: 'Azaan CLI',
|
|
54
|
+
icon: ICON_PATH,
|
|
55
|
+
title: `Azaan Time: ${next.name}`,
|
|
56
|
+
message: `It is now time for ${next.name} (${formatTime12h(next.time)}).`,
|
|
57
|
+
sound: true,
|
|
58
|
+
wait: false
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// Fail silently in background
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Initial run
|
|
68
|
+
runDaemon();
|
|
69
|
+
|
|
70
|
+
// Start polling
|
|
71
|
+
setInterval(runDaemon, INTERVAL_MS);
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { intro, text, select, spinner, isCancel, cancel, outro } from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import Table from 'cli-table3';
|
|
4
|
+
import { formatTime12h, getNextPrayer, formatCountdownVerbose } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export async function runInteractiveSetup(currentConfig = {}) {
|
|
7
|
+
intro(chalk.bgGreen.white(' Azaan CLI Setup '));
|
|
8
|
+
|
|
9
|
+
const city = await text({
|
|
10
|
+
message: 'Which city are you in?',
|
|
11
|
+
placeholder: 'e.g. Lahore, London, New York',
|
|
12
|
+
initialValue: currentConfig.city || '',
|
|
13
|
+
validate(value) {
|
|
14
|
+
if (value.length === 0) return `City is required!`;
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (isCancel(city)) {
|
|
19
|
+
cancel('Setup cancelled.');
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const country = await text({
|
|
24
|
+
message: 'Which country?',
|
|
25
|
+
placeholder: 'e.g. Pakistan, UK, US',
|
|
26
|
+
initialValue: currentConfig.country || '',
|
|
27
|
+
validate(value) {
|
|
28
|
+
if (value.length === 0) return `Country is required!`;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (isCancel(country)) {
|
|
33
|
+
cancel('Setup cancelled.');
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const method = await select({
|
|
38
|
+
message: 'Calculation Method',
|
|
39
|
+
options: [
|
|
40
|
+
{ value: 1, label: 'University of Islamic Sciences, Karachi' },
|
|
41
|
+
{ value: 2, label: 'Islamic Society of North America (ISNA)' },
|
|
42
|
+
{ value: 3, label: 'Muslim World League' },
|
|
43
|
+
{ value: 4, label: 'Umm Al-Qura University, Makkah' },
|
|
44
|
+
{ value: 5, label: 'Egyptian General Authority of Survey' },
|
|
45
|
+
{ value: 8, label: 'Gulf Region' },
|
|
46
|
+
{ value: 9, label: 'Kuwait' },
|
|
47
|
+
{ value: 10, label: 'Qatar' },
|
|
48
|
+
{ value: 11, label: 'Majlis Ugama Islam Singapura, Singapore' },
|
|
49
|
+
{ value: 12, label: 'Union Organization islamic de France' },
|
|
50
|
+
{ value: 13, label: 'Diyanet İşleri Başkanlığı, Turkey' },
|
|
51
|
+
{ value: 14, label: 'Spiritual Administration of Muslims of Russia' }
|
|
52
|
+
],
|
|
53
|
+
initialValue: currentConfig.method || 2,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (isCancel(method)) {
|
|
57
|
+
cancel('Setup cancelled.');
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
outro('Setup complete! Configuration saved.');
|
|
62
|
+
|
|
63
|
+
return { city, country, method };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function printTimingsTable(timingsObj, city) {
|
|
67
|
+
console.log(chalk.cyan(`
|
|
68
|
+
█████╗ ███████╗ █████╗ █████╗ ███╗ ██╗
|
|
69
|
+
██╔══██╗╚══███╔╝██╔══██╗██╔══██╗████╗ ██║
|
|
70
|
+
███████║ ███╔╝ ███████║███████║██╔██╗ ██║
|
|
71
|
+
██╔══██║ ███╔╝ ██╔══██║██╔══██║██║╚██╗██║
|
|
72
|
+
██║ ██║███████╗██║ ██║██║ ██║██║ ╚████║
|
|
73
|
+
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝
|
|
74
|
+
`));
|
|
75
|
+
console.log(` 🕌 ${chalk.bold('Daily Prayer Timings')}`);
|
|
76
|
+
console.log(` 📍 ${chalk.green.bold(city)} (${chalk.gray('Today')})\n`);
|
|
77
|
+
|
|
78
|
+
const table = new Table({
|
|
79
|
+
head: [chalk.bold.blue('Prayer'), chalk.bold.blue('Time')],
|
|
80
|
+
colWidths: [20, 20],
|
|
81
|
+
chars: {
|
|
82
|
+
'top': '', 'top-mid': '', 'top-left': '', 'top-right': '',
|
|
83
|
+
'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '',
|
|
84
|
+
'left': ' ', 'left-mid': '', 'mid': '', 'mid-mid': '',
|
|
85
|
+
'right': '', 'right-mid': '', 'middle': ' '
|
|
86
|
+
},
|
|
87
|
+
style: { 'padding-left': 0, 'padding-right': 0, head: [], border: [] }
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const nextPrayer = getNextPrayer(timingsObj);
|
|
91
|
+
const prayers = ['Fajr', 'Dhuhr', 'Asr', 'Maghrib', 'Isha'];
|
|
92
|
+
|
|
93
|
+
prayers.forEach((prayer) => {
|
|
94
|
+
if (timingsObj[prayer]) {
|
|
95
|
+
// Remove timezone string like "(EST)"
|
|
96
|
+
const rawTime = timingsObj[prayer].split(' ')[0];
|
|
97
|
+
const time12h = formatTime12h(rawTime);
|
|
98
|
+
|
|
99
|
+
let pName = prayer;
|
|
100
|
+
let pTime = time12h;
|
|
101
|
+
|
|
102
|
+
if (nextPrayer && nextPrayer.name === prayer) {
|
|
103
|
+
pName = chalk.bgYellow.black(` ${prayer} (Next) `);
|
|
104
|
+
pTime = chalk.yellow.bold(time12h);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
table.push([pName, pTime]);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log(table.toString());
|
|
112
|
+
|
|
113
|
+
if (nextPrayer) {
|
|
114
|
+
console.log(`\n Next prayer is ${chalk.bold.yellow(nextPrayer.name)} in ${chalk.bold.cyan(formatCountdownVerbose(nextPrayer.diffSeconds))} at ${chalk.bold.magenta(formatTime12h(nextPrayer.time))}\n`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function createSpinner(msg) {
|
|
119
|
+
const s = spinner();
|
|
120
|
+
s.start(msg);
|
|
121
|
+
return s;
|
|
122
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { parse, format, differenceInSeconds, isAfter, isBefore } from 'date-fns';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts HH:mm (24-hour) format into a 12-hour AM/PM string.
|
|
5
|
+
*/
|
|
6
|
+
export function formatTime12h(time24) {
|
|
7
|
+
const [hours, minutes] = time24.split(':');
|
|
8
|
+
const d = new Date();
|
|
9
|
+
d.setHours(parseInt(hours, 10), parseInt(minutes, 10));
|
|
10
|
+
return format(d, 'hh:mm a');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Given a list of timings from Aladhan API, find the next prayer.
|
|
15
|
+
* Returns { name, time, diffSeconds } or null.
|
|
16
|
+
*/
|
|
17
|
+
export function getNextPrayer(timingsObj) {
|
|
18
|
+
const now = new Date();
|
|
19
|
+
|
|
20
|
+
// The prayers we care about
|
|
21
|
+
const prayers = ['Fajr', 'Dhuhr', 'Asr', 'Maghrib', 'Isha'];
|
|
22
|
+
|
|
23
|
+
let nextPrayer = null;
|
|
24
|
+
let minDiff = Infinity;
|
|
25
|
+
|
|
26
|
+
// Clean the timings (Aladhan sometimes appends (PKT) etc)
|
|
27
|
+
for (const prayer of prayers) {
|
|
28
|
+
if (!timingsObj[prayer]) continue;
|
|
29
|
+
|
|
30
|
+
const timeStr = timingsObj[prayer].split(' ')[0]; // removes " (EST)"
|
|
31
|
+
const [hours, minutes] = timeStr.split(':');
|
|
32
|
+
|
|
33
|
+
const prayerTime = new Date();
|
|
34
|
+
prayerTime.setHours(parseInt(hours, 10), parseInt(minutes, 10), 0, 0);
|
|
35
|
+
|
|
36
|
+
if (isAfter(prayerTime, now)) {
|
|
37
|
+
const diffSecs = differenceInSeconds(prayerTime, now);
|
|
38
|
+
if (diffSecs < minDiff) {
|
|
39
|
+
minDiff = diffSecs;
|
|
40
|
+
nextPrayer = {
|
|
41
|
+
name: prayer,
|
|
42
|
+
time: timeStr,
|
|
43
|
+
diffSeconds: diffSecs,
|
|
44
|
+
dateObj: prayerTime
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If no next prayer today, the next is Fajr tomorrow
|
|
51
|
+
// (We'll handle tomorrow Fajr logic loosely by just saying "Tomorrow's Fajr" or leaving null)
|
|
52
|
+
return nextPrayer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format seconds into HH:MM:SS
|
|
57
|
+
*/
|
|
58
|
+
export function formatCountdown(totalSeconds) {
|
|
59
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
60
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
61
|
+
const seconds = totalSeconds % 60;
|
|
62
|
+
|
|
63
|
+
const pad = (num) => String(num).padStart(2, '0');
|
|
64
|
+
|
|
65
|
+
if (hours > 0) {
|
|
66
|
+
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
|
67
|
+
}
|
|
68
|
+
return `${pad(minutes)}:${pad(seconds)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format seconds into verbose text (e.g. 45 min and 12 second)
|
|
73
|
+
*/
|
|
74
|
+
export function formatCountdownVerbose(totalSeconds) {
|
|
75
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
76
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
77
|
+
const seconds = totalSeconds % 60;
|
|
78
|
+
|
|
79
|
+
let parts = [];
|
|
80
|
+
if (hours > 0) parts.push(`${hours} hr${hours > 1 ? 's' : ''}`);
|
|
81
|
+
if (minutes > 0) parts.push(`${minutes} min${minutes > 1 ? 's' : ''}`);
|
|
82
|
+
if (seconds > 0) parts.push(`${seconds} second${seconds > 1 ? 's' : ''}`);
|
|
83
|
+
|
|
84
|
+
if (parts.length === 0) return 'now';
|
|
85
|
+
if (parts.length === 1) return parts[0];
|
|
86
|
+
if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
|
|
87
|
+
|
|
88
|
+
const last = parts.pop();
|
|
89
|
+
return `${parts.join(', ')} and ${last}`;
|
|
90
|
+
}
|