@daliovic/cc-statusline 1.1.1 β 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 +31 -4
- package/dist/config.js +24 -0
- package/dist/prayer.js +123 -0
- package/dist/statusline.js +9 -0
- package/dist/wizard.js +79 -1
- package/package.json +1 -1
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,8 @@ 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)
|
|
16
|
+
- **Customizable** - Interactive wizard to configure colors, visibility, and thresholds
|
|
15
17
|
|
|
16
18
|
## Installation
|
|
17
19
|
|
|
@@ -70,6 +72,31 @@ This lets you customize:
|
|
|
70
72
|
|
|
71
73
|
Config is saved to `~/.claude/cc-statusline.json`.
|
|
72
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
|
+
|
|
73
100
|
### Environment Variables
|
|
74
101
|
|
|
75
102
|
| Variable | Default | Description |
|
|
@@ -86,13 +113,13 @@ Config is saved to `~/.claude/cc-statusline.json`.
|
|
|
86
113
|
## Output Format
|
|
87
114
|
|
|
88
115
|
```
|
|
89
|
-
π€ Model β π Context% Tokens β β± 5hr% Time Delta/7day% Time Delta
|
|
116
|
+
π€ Model β π Context% Tokens β β± 5hr% Time Delta/7day% Time Delta β π Prayer Time (countdown)
|
|
90
117
|
```
|
|
91
118
|
|
|
92
|
-
**Colors
|
|
119
|
+
**Default Colors** (customizable via `--config`):
|
|
93
120
|
- Cyan: Model name and icons
|
|
94
121
|
- Gray: Percentages and times
|
|
95
|
-
- Orange: Context at
|
|
122
|
+
- Orange: Context at threshold
|
|
96
123
|
- Green βΌ: Under budget
|
|
97
124
|
- Red β²: Over budget
|
|
98
125
|
|
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
|
+
}
|
package/dist/statusline.js
CHANGED
|
@@ -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?",
|