@daliovic/cc-statusline 1.1.2 β†’ 1.2.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  A minimal, informative statusline for [Claude Code](https://claude.ai/claude-code) CLI.
4
4
 
5
5
  ```
6
- πŸ€– Opus 4.5 β”‚ πŸ“Š 53% 106K β”‚ ⏱ 26% 2h09 β–Ό1h32/46% 2d15 β–Ό1d04
6
+ πŸ€– Opus 4.5 β”‚ πŸ“Š 53% 106K β”‚ ⏱ 26% 2h09 β–Ό1h32/46% 2d15 β–Ό1d04 β”‚ πŸ•Œ Asr 15:32 (2h15)
7
7
  ```
8
8
 
9
9
  ## Features
@@ -12,6 +12,7 @@ A minimal, informative statusline for [Claude Code](https://claude.ai/claude-cod
12
12
  - **Context usage** - Percentage + token count (turns orange at 75%)
13
13
  - **Usage limits** - 5-hour session and 7-day limits with reset times
14
14
  - **Budget delta** - Shows if you're under (β–Ό green) or over (β–² red) your expected usage rate
15
+ - **Prayer times** - Next prayer name, time, and countdown (auto-detects location)
15
16
  - **Customizable** - Interactive wizard to configure colors, visibility, and thresholds
16
17
 
17
18
  ## Installation
@@ -71,6 +72,31 @@ This lets you customize:
71
72
 
72
73
  Config is saved to `~/.claude/cc-statusline.json`.
73
74
 
75
+ ### Prayer Times
76
+
77
+ Prayer times are enabled by default and auto-detect your location via IP. Configure via `--config`:
78
+
79
+ - **Calculation method** - Umm Al-Qura (default), ISNA, Muslim World League, and more
80
+ - **Location** - Auto-detect or manual lat/lng
81
+
82
+ Supported calculation methods:
83
+ | Code | Method |
84
+ |------|--------|
85
+ | 0 | Shia Ithna-Ansari |
86
+ | 1 | University of Islamic Sciences, Karachi |
87
+ | 2 | Islamic Society of North America (ISNA) |
88
+ | 3 | Muslim World League |
89
+ | 4 | Umm Al-Qura, Makkah (default) |
90
+ | 5 | Egyptian General Authority |
91
+ | 7 | Institute of Geophysics, Tehran |
92
+ | 8 | Gulf Region |
93
+ | 9 | Kuwait |
94
+ | 10 | Qatar |
95
+ | 11 | Majlis Ugama Islam Singapura |
96
+ | 12 | UOIF France |
97
+ | 13 | Diyanet Turkey |
98
+ | 14 | Spiritual Administration of Muslims of Russia |
99
+
74
100
  ### Environment Variables
75
101
 
76
102
  | Variable | Default | Description |
@@ -87,7 +113,7 @@ Config is saved to `~/.claude/cc-statusline.json`.
87
113
  ## Output Format
88
114
 
89
115
  ```
90
- πŸ€– Model β”‚ πŸ“Š Context% Tokens β”‚ ⏱ 5hr% Time Delta/7day% Time Delta
116
+ πŸ€– Model β”‚ πŸ“Š Context% Tokens β”‚ ⏱ 5hr% Time Delta/7day% Time Delta β”‚ πŸ•Œ Prayer Time (countdown)
91
117
  ```
92
118
 
93
119
  **Default Colors** (customizable via `--config`):
package/dist/config.js CHANGED
@@ -2,6 +2,23 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join, dirname } from "node:path";
4
4
  export const CONFIG_PATH = join(homedir(), ".claude", "cc-statusline.json");
5
+ export const PRAYER_CACHE_PATH = join(homedir(), ".claude", "cc-statusline-prayer.json");
6
+ export const PRAYER_METHODS = {
7
+ 0: "Shia Ithna-Ansari",
8
+ 1: "University of Islamic Sciences, Karachi",
9
+ 2: "Islamic Society of North America (ISNA)",
10
+ 3: "Muslim World League",
11
+ 4: "Umm Al-Qura, Makkah",
12
+ 5: "Egyptian General Authority of Survey",
13
+ 7: "Institute of Geophysics, University of Tehran",
14
+ 8: "Gulf Region",
15
+ 9: "Kuwait",
16
+ 10: "Qatar",
17
+ 11: "Majlis Ugama Islam Singapura",
18
+ 12: "Union Organization Islamic de France",
19
+ 13: "Diyanet İşleri Başkanlığı, Turkey",
20
+ 14: "Spiritual Administration of Muslims of Russia",
21
+ };
5
22
  export const DEFAULT_CONFIG = {
6
23
  show: {
7
24
  model: true,
@@ -9,6 +26,7 @@ export const DEFAULT_CONFIG = {
9
26
  usage5hr: true,
10
27
  usage7day: true,
11
28
  delta: true,
29
+ prayer: true,
12
30
  },
13
31
  thresholds: {
14
32
  contextWarning: 75,
@@ -21,6 +39,11 @@ export const DEFAULT_CONFIG = {
21
39
  usage: 248, // gray
22
40
  deltaUnder: 32, // green
23
41
  deltaOver: 31, // red
42
+ prayer: 36, // cyan
43
+ },
44
+ prayer: {
45
+ method: 4, // Umm Al-Qura
46
+ location: null, // auto-detect via IP
24
47
  },
25
48
  };
26
49
  export function loadConfig() {
@@ -35,6 +58,7 @@ export function loadConfig() {
35
58
  show: { ...DEFAULT_CONFIG.show, ...loaded.show },
36
59
  thresholds: { ...DEFAULT_CONFIG.thresholds, ...loaded.thresholds },
37
60
  colors: { ...DEFAULT_CONFIG.colors, ...loaded.colors },
61
+ prayer: { ...DEFAULT_CONFIG.prayer, ...loaded.prayer },
38
62
  };
39
63
  }
40
64
  catch {
package/dist/prayer.js ADDED
@@ -0,0 +1,123 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { PRAYER_CACHE_PATH } from "./config.js";
3
+ const PRAYER_ORDER = ["Fajr", "Sunrise", "Dhuhr", "Asr", "Maghrib", "Isha"];
4
+ function getTodayDate() {
5
+ const now = new Date();
6
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
7
+ }
8
+ function parseTime(timeStr) {
9
+ const [hours, minutes] = timeStr.split(":").map(Number);
10
+ const now = new Date();
11
+ now.setHours(hours, minutes, 0, 0);
12
+ return now;
13
+ }
14
+ function formatTimeLeft(diffMs) {
15
+ if (diffMs <= 0)
16
+ return "0m";
17
+ const totalMins = Math.floor(diffMs / (1000 * 60));
18
+ const hours = Math.floor(totalMins / 60);
19
+ const mins = totalMins % 60;
20
+ if (hours > 0) {
21
+ return `${hours}h${String(mins).padStart(2, "0")}`;
22
+ }
23
+ return `${mins}m`;
24
+ }
25
+ async function getLocationFromIP() {
26
+ try {
27
+ const res = await fetch("http://ip-api.com/json/?fields=lat,lon");
28
+ if (!res.ok)
29
+ return null;
30
+ const data = await res.json();
31
+ return { latitude: data.lat, longitude: data.lon };
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ async function fetchPrayerTimesFromAPI(location, method) {
38
+ try {
39
+ const timestamp = Math.floor(Date.now() / 1000);
40
+ const url = `https://api.aladhan.com/v1/timings/${timestamp}?latitude=${location.latitude}&longitude=${location.longitude}&method=${method}`;
41
+ const res = await fetch(url);
42
+ if (!res.ok)
43
+ return null;
44
+ const data = await res.json();
45
+ return data.data.timings;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ function loadCache() {
52
+ try {
53
+ if (!existsSync(PRAYER_CACHE_PATH))
54
+ return null;
55
+ const content = readFileSync(PRAYER_CACHE_PATH, "utf-8");
56
+ return JSON.parse(content);
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ function saveCache(cache) {
63
+ try {
64
+ writeFileSync(PRAYER_CACHE_PATH, JSON.stringify(cache));
65
+ }
66
+ catch {
67
+ // Ignore cache write errors
68
+ }
69
+ }
70
+ function isCacheValid(cache, location, method) {
71
+ return (cache.date === getTodayDate() &&
72
+ cache.method === method &&
73
+ Math.abs(cache.location.latitude - location.latitude) < 0.1 &&
74
+ Math.abs(cache.location.longitude - location.longitude) < 0.1);
75
+ }
76
+ export async function getNextPrayer(configLocation, method) {
77
+ // Get location
78
+ const location = configLocation ?? (await getLocationFromIP());
79
+ if (!location)
80
+ return null;
81
+ // Check cache
82
+ const cache = loadCache();
83
+ let times;
84
+ if (cache && isCacheValid(cache, location, method)) {
85
+ times = cache.times;
86
+ }
87
+ else {
88
+ // Fetch fresh data
89
+ const fetched = await fetchPrayerTimesFromAPI(location, method);
90
+ if (!fetched)
91
+ return null;
92
+ times = fetched;
93
+ // Save to cache
94
+ saveCache({
95
+ date: getTodayDate(),
96
+ location,
97
+ method,
98
+ times,
99
+ });
100
+ }
101
+ // Find next prayer
102
+ const now = new Date();
103
+ for (const name of PRAYER_ORDER) {
104
+ const prayerTime = parseTime(times[name]);
105
+ if (prayerTime > now) {
106
+ const diffMs = prayerTime.getTime() - now.getTime();
107
+ return {
108
+ name,
109
+ time: times[name],
110
+ timeLeft: formatTimeLeft(diffMs),
111
+ };
112
+ }
113
+ }
114
+ // All prayers passed, return tomorrow's Fajr
115
+ const tomorrowFajr = parseTime(times.Fajr);
116
+ tomorrowFajr.setDate(tomorrowFajr.getDate() + 1);
117
+ const diffMs = tomorrowFajr.getTime() - now.getTime();
118
+ return {
119
+ name: "Fajr",
120
+ time: times.Fajr,
121
+ timeLeft: formatTimeLeft(diffMs),
122
+ };
123
+ }
@@ -3,6 +3,7 @@ import { readFileSync, writeFileSync, existsSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { loadConfig } from "./config.js";
6
+ import { getNextPrayer } from "./prayer.js";
6
7
  // Handle --config flag first and exit
7
8
  if (process.argv.includes("--config")) {
8
9
  const { runWizard } = await import("./wizard.js");
@@ -27,6 +28,7 @@ const color = {
27
28
  usage: `\x1b[38;5;${userConfig.colors.usage}m`,
28
29
  deltaUnder: `\x1b[38;5;${userConfig.colors.deltaUnder}m`,
29
30
  deltaOver: `\x1b[38;5;${userConfig.colors.deltaOver}m`,
31
+ prayer: `\x1b[38;5;${userConfig.colors.prayer}m`,
30
32
  };
31
33
  // === Helpers ===
32
34
  function formatTokens(tokens) {
@@ -240,6 +242,13 @@ async function main() {
240
242
  segments.push(`${color.model}\u{23F1}${color.reset} ${parts.join(`${color.dim}/${color.reset}`)}`);
241
243
  }
242
244
  }
245
+ // Prayer times
246
+ if (userConfig.show.prayer) {
247
+ const prayer = await getNextPrayer(userConfig.prayer.location, userConfig.prayer.method);
248
+ if (prayer) {
249
+ segments.push(`${color.prayer}\u{1F54C} ${prayer.name} ${prayer.time}${color.dim} (${prayer.timeLeft})${color.reset}`);
250
+ }
251
+ }
243
252
  // Output
244
253
  console.log(segments.join(` ${color.dim}\u{2502}${color.reset} `));
245
254
  }
package/dist/wizard.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { select, checkbox, input, confirm } from "@inquirer/prompts";
2
- import { DEFAULT_CONFIG, loadConfig, saveConfig, CONFIG_PATH } from "./config.js";
2
+ import { DEFAULT_CONFIG, loadConfig, saveConfig, CONFIG_PATH, PRAYER_METHODS } from "./config.js";
3
3
  // ANSI color helper
4
4
  function colorize(code, text) {
5
5
  return `\x1b[38;5;${code}m${text}\x1b[0m`;
@@ -26,6 +26,9 @@ function showPreview(config) {
26
26
  parts.push(`\u{23F1} ${usageParts.join("/")}`);
27
27
  }
28
28
  }
29
+ if (config.show.prayer) {
30
+ parts.push(colorize(c.prayer, "\u{1F54C} Asr 15:32") + " \x1b[2m(2h15)\x1b[0m");
31
+ }
29
32
  console.log("\n Preview: " + parts.join(" \x1b[2m\u{2502}\x1b[0m ") + "\n");
30
33
  }
31
34
  async function visibilityMenu(config) {
@@ -35,6 +38,7 @@ async function visibilityMenu(config) {
35
38
  { name: "5-hour usage limit", value: "usage5hr", checked: config.show.usage5hr },
36
39
  { name: "7-day usage limit", value: "usage7day", checked: config.show.usage7day },
37
40
  { name: "Budget delta arrows", value: "delta", checked: config.show.delta },
41
+ { name: "Prayer times", value: "prayer", checked: config.show.prayer },
38
42
  ];
39
43
  const selected = await checkbox({
40
44
  message: "Toggle items to show (space to toggle, enter to confirm)",
@@ -45,6 +49,7 @@ async function visibilityMenu(config) {
45
49
  config.show.usage5hr = selected.includes("usage5hr");
46
50
  config.show.usage7day = selected.includes("usage7day");
47
51
  config.show.delta = selected.includes("delta");
52
+ config.show.prayer = selected.includes("prayer");
48
53
  }
49
54
  async function colorsMenu(config) {
50
55
  const colorOptions = [
@@ -54,6 +59,7 @@ async function colorsMenu(config) {
54
59
  { name: `Usage (current: ${colorize(config.colors.usage, String(config.colors.usage))})`, value: "usage" },
55
60
  { name: `Delta under budget (current: ${colorize(config.colors.deltaUnder, String(config.colors.deltaUnder))})`, value: "deltaUnder" },
56
61
  { name: `Delta over budget (current: ${colorize(config.colors.deltaOver, String(config.colors.deltaOver))})`, value: "deltaOver" },
62
+ { name: `Prayer (current: ${colorize(config.colors.prayer, String(config.colors.prayer))})`, value: "prayer" },
57
63
  { name: "Back", value: "back" },
58
64
  ];
59
65
  while (true) {
@@ -83,6 +89,74 @@ async function colorsMenu(config) {
83
89
  }
84
90
  }
85
91
  }
92
+ async function prayerMenu(config) {
93
+ const methodChoices = Object.entries(PRAYER_METHODS).map(([code, name]) => ({
94
+ name: `${name}${Number(code) === config.prayer.method ? " (current)" : ""}`,
95
+ value: code,
96
+ }));
97
+ methodChoices.push({ name: "Back", value: "back" });
98
+ while (true) {
99
+ const choice = await select({
100
+ message: "Prayer settings",
101
+ choices: [
102
+ { name: `Calculation method: ${PRAYER_METHODS[config.prayer.method]}`, value: "method" },
103
+ { name: config.prayer.location
104
+ ? `Location: ${config.prayer.location.latitude}, ${config.prayer.location.longitude}`
105
+ : "Location: Auto-detect (IP)", value: "location" },
106
+ { name: "Back", value: "back" },
107
+ ],
108
+ });
109
+ if (choice === "back")
110
+ break;
111
+ if (choice === "method") {
112
+ const method = await select({
113
+ message: "Select calculation method",
114
+ choices: methodChoices,
115
+ });
116
+ if (method !== "back") {
117
+ config.prayer.method = Number(method);
118
+ }
119
+ }
120
+ else if (choice === "location") {
121
+ const locChoice = await select({
122
+ message: "Location setting",
123
+ choices: [
124
+ { name: "Auto-detect from IP (recommended)", value: "auto" },
125
+ { name: "Enter manually", value: "manual" },
126
+ ],
127
+ });
128
+ if (locChoice === "auto") {
129
+ config.prayer.location = null;
130
+ }
131
+ else {
132
+ const lat = await input({
133
+ message: "Latitude",
134
+ default: config.prayer.location?.latitude?.toString() || "",
135
+ validate: (val) => {
136
+ const num = parseFloat(val);
137
+ if (isNaN(num) || num < -90 || num > 90)
138
+ return "Enter -90 to 90";
139
+ return true;
140
+ },
141
+ });
142
+ const lng = await input({
143
+ message: "Longitude",
144
+ default: config.prayer.location?.longitude?.toString() || "",
145
+ validate: (val) => {
146
+ const num = parseFloat(val);
147
+ if (isNaN(num) || num < -180 || num > 180)
148
+ return "Enter -180 to 180";
149
+ return true;
150
+ },
151
+ });
152
+ config.prayer.location = {
153
+ latitude: parseFloat(lat),
154
+ longitude: parseFloat(lng),
155
+ };
156
+ }
157
+ }
158
+ }
159
+ }
86
160
  async function thresholdsMenu(config) {
87
161
  const contextWarning = await input({
88
162
  message: "Context warning threshold % (turns orange)",
@@ -118,6 +192,7 @@ export async function runWizard() {
118
192
  { name: "Visibility (show/hide items)", value: "visibility" },
119
193
  { name: "Colors", value: "colors" },
120
194
  { name: "Thresholds", value: "thresholds" },
195
+ { name: "Prayer settings", value: "prayer" },
121
196
  { name: "Reset to defaults", value: "reset" },
122
197
  { name: "Save & Exit", value: "save" },
123
198
  { name: "Exit without saving", value: "exit" },
@@ -135,6 +210,9 @@ export async function runWizard() {
135
210
  case "thresholds":
136
211
  await thresholdsMenu(config);
137
212
  break;
213
+ case "prayer":
214
+ await prayerMenu(config);
215
+ break;
138
216
  case "reset":
139
217
  const confirmReset = await confirm({
140
218
  message: "Reset all settings to defaults?",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daliovic/cc-statusline",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Minimal Claude Code statusline with usage limits and budget tracking",
5
5
  "type": "module",
6
6
  "main": "dist/statusline.js",