@daliovic/cc-statusline 1.1.2 β 1.3.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 +37 -2
- package/dist/config.js +37 -0
- package/dist/prayer.js +145 -0
- package/dist/statusline.js +9 -0
- package/dist/wizard.js +309 -113
- 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,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,40 @@ 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
|
+
| 15 | Moonsighting Committee Worldwide |
|
|
100
|
+
| 16 | Dubai (experimental) |
|
|
101
|
+
| 17 | JAKIM, Malaysia |
|
|
102
|
+
| 18 | Tunisia |
|
|
103
|
+
| 19 | Algeria |
|
|
104
|
+
| 20 | KEMENAG, Indonesia |
|
|
105
|
+
| 21 | Morocco |
|
|
106
|
+
| 22 | Comunidade Islamica de Lisboa |
|
|
107
|
+
| 23 | Ministry of Awqaf, Jordan |
|
|
108
|
+
|
|
74
109
|
### Environment Variables
|
|
75
110
|
|
|
76
111
|
| Variable | Default | Description |
|
|
@@ -87,7 +122,7 @@ Config is saved to `~/.claude/cc-statusline.json`.
|
|
|
87
122
|
## Output Format
|
|
88
123
|
|
|
89
124
|
```
|
|
90
|
-
π€ Model β π Context% Tokens β β± 5hr% Time Delta/7day% Time Delta
|
|
125
|
+
π€ Model β π Context% Tokens β β± 5hr% Time Delta/7day% Time Delta β π Prayer Time (countdown)
|
|
91
126
|
```
|
|
92
127
|
|
|
93
128
|
**Default Colors** (customizable via `--config`):
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,32 @@ 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
|
+
15: "Moonsighting Committee Worldwide",
|
|
22
|
+
16: "Dubai (experimental)",
|
|
23
|
+
17: "JAKIM, Malaysia",
|
|
24
|
+
18: "Tunisia",
|
|
25
|
+
19: "Algeria",
|
|
26
|
+
20: "KEMENAG, Indonesia",
|
|
27
|
+
21: "Morocco",
|
|
28
|
+
22: "Comunidade Islamica de Lisboa",
|
|
29
|
+
23: "Ministry of Awqaf, Jordan",
|
|
30
|
+
};
|
|
5
31
|
export const DEFAULT_CONFIG = {
|
|
6
32
|
show: {
|
|
7
33
|
model: true,
|
|
@@ -9,11 +35,15 @@ export const DEFAULT_CONFIG = {
|
|
|
9
35
|
usage5hr: true,
|
|
10
36
|
usage7day: true,
|
|
11
37
|
delta: true,
|
|
38
|
+
prayer: true,
|
|
12
39
|
},
|
|
13
40
|
thresholds: {
|
|
14
41
|
contextWarning: 75,
|
|
15
42
|
cacheTtlMs: 300000,
|
|
16
43
|
},
|
|
44
|
+
wizard: {
|
|
45
|
+
autosave: true,
|
|
46
|
+
},
|
|
17
47
|
colors: {
|
|
18
48
|
model: 36, // cyan
|
|
19
49
|
context: 248, // gray
|
|
@@ -21,6 +51,11 @@ export const DEFAULT_CONFIG = {
|
|
|
21
51
|
usage: 248, // gray
|
|
22
52
|
deltaUnder: 32, // green
|
|
23
53
|
deltaOver: 31, // red
|
|
54
|
+
prayer: 36, // cyan
|
|
55
|
+
},
|
|
56
|
+
prayer: {
|
|
57
|
+
method: 4, // Umm Al-Qura
|
|
58
|
+
location: null, // auto-detect via IP
|
|
24
59
|
},
|
|
25
60
|
};
|
|
26
61
|
export function loadConfig() {
|
|
@@ -34,7 +69,9 @@ export function loadConfig() {
|
|
|
34
69
|
return {
|
|
35
70
|
show: { ...DEFAULT_CONFIG.show, ...loaded.show },
|
|
36
71
|
thresholds: { ...DEFAULT_CONFIG.thresholds, ...loaded.thresholds },
|
|
72
|
+
wizard: { ...DEFAULT_CONFIG.wizard, ...loaded.wizard },
|
|
37
73
|
colors: { ...DEFAULT_CONFIG.colors, ...loaded.colors },
|
|
74
|
+
prayer: { ...DEFAULT_CONFIG.prayer, ...loaded.prayer },
|
|
38
75
|
};
|
|
39
76
|
}
|
|
40
77
|
catch {
|
package/dist/prayer.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
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,city,country");
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
return null;
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
return { latitude: data.lat, longitude: data.lon, city: data.city, country: data.country };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function geocodeCity(city, country) {
|
|
38
|
+
try {
|
|
39
|
+
const query = encodeURIComponent(`${city}, ${country}`);
|
|
40
|
+
const res = await fetch(`https://nominatim.openstreetmap.org/search?q=${query}&format=json&limit=1`, {
|
|
41
|
+
headers: { "User-Agent": "cc-statusline/1.0" },
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok)
|
|
44
|
+
return null;
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
if (!data.length)
|
|
47
|
+
return null;
|
|
48
|
+
return {
|
|
49
|
+
latitude: parseFloat(data[0].lat),
|
|
50
|
+
longitude: parseFloat(data[0].lon),
|
|
51
|
+
city,
|
|
52
|
+
country,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function fetchPrayerTimesFromAPI(location, method) {
|
|
60
|
+
try {
|
|
61
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
62
|
+
const url = `https://api.aladhan.com/v1/timings/${timestamp}?latitude=${location.latitude}&longitude=${location.longitude}&method=${method}`;
|
|
63
|
+
const res = await fetch(url);
|
|
64
|
+
if (!res.ok)
|
|
65
|
+
return null;
|
|
66
|
+
const data = await res.json();
|
|
67
|
+
return data.data.timings;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function loadCache() {
|
|
74
|
+
try {
|
|
75
|
+
if (!existsSync(PRAYER_CACHE_PATH))
|
|
76
|
+
return null;
|
|
77
|
+
const content = readFileSync(PRAYER_CACHE_PATH, "utf-8");
|
|
78
|
+
return JSON.parse(content);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function saveCache(cache) {
|
|
85
|
+
try {
|
|
86
|
+
writeFileSync(PRAYER_CACHE_PATH, JSON.stringify(cache));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Ignore cache write errors
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function isCacheValid(cache, location, method) {
|
|
93
|
+
return (cache.date === getTodayDate() &&
|
|
94
|
+
cache.method === method &&
|
|
95
|
+
Math.abs(cache.location.latitude - location.latitude) < 0.1 &&
|
|
96
|
+
Math.abs(cache.location.longitude - location.longitude) < 0.1);
|
|
97
|
+
}
|
|
98
|
+
export async function getNextPrayer(configLocation, method) {
|
|
99
|
+
// Get location
|
|
100
|
+
const location = configLocation ?? (await getLocationFromIP());
|
|
101
|
+
if (!location)
|
|
102
|
+
return null;
|
|
103
|
+
// Check cache
|
|
104
|
+
const cache = loadCache();
|
|
105
|
+
let times;
|
|
106
|
+
if (cache && isCacheValid(cache, location, method)) {
|
|
107
|
+
times = cache.times;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// Fetch fresh data
|
|
111
|
+
const fetched = await fetchPrayerTimesFromAPI(location, method);
|
|
112
|
+
if (!fetched)
|
|
113
|
+
return null;
|
|
114
|
+
times = fetched;
|
|
115
|
+
// Save to cache
|
|
116
|
+
saveCache({
|
|
117
|
+
date: getTodayDate(),
|
|
118
|
+
location,
|
|
119
|
+
method,
|
|
120
|
+
times,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// Find next prayer
|
|
124
|
+
const now = new Date();
|
|
125
|
+
for (const name of PRAYER_ORDER) {
|
|
126
|
+
const prayerTime = parseTime(times[name]);
|
|
127
|
+
if (prayerTime > now) {
|
|
128
|
+
const diffMs = prayerTime.getTime() - now.getTime();
|
|
129
|
+
return {
|
|
130
|
+
name,
|
|
131
|
+
time: times[name],
|
|
132
|
+
timeLeft: formatTimeLeft(diffMs),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// All prayers passed, return tomorrow's Fajr
|
|
137
|
+
const tomorrowFajr = parseTime(times.Fajr);
|
|
138
|
+
tomorrowFajr.setDate(tomorrowFajr.getDate() + 1);
|
|
139
|
+
const diffMs = tomorrowFajr.getTime() - now.getTime();
|
|
140
|
+
return {
|
|
141
|
+
name: "Fajr",
|
|
142
|
+
time: times.Fajr,
|
|
143
|
+
timeLeft: formatTimeLeft(diffMs),
|
|
144
|
+
};
|
|
145
|
+
}
|
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,9 +1,17 @@
|
|
|
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`;
|
|
6
6
|
}
|
|
7
|
+
// Handle Ctrl+C cancellation
|
|
8
|
+
function isCancelled(error) {
|
|
9
|
+
if (!(error instanceof Error))
|
|
10
|
+
return false;
|
|
11
|
+
return error.name === "ExitPromptError" ||
|
|
12
|
+
error.name === "AbortError" ||
|
|
13
|
+
error.message.includes("force closed");
|
|
14
|
+
}
|
|
7
15
|
function showPreview(config) {
|
|
8
16
|
const c = config.colors;
|
|
9
17
|
const parts = [];
|
|
@@ -26,138 +34,326 @@ function showPreview(config) {
|
|
|
26
34
|
parts.push(`\u{23F1} ${usageParts.join("/")}`);
|
|
27
35
|
}
|
|
28
36
|
}
|
|
37
|
+
if (config.show.prayer) {
|
|
38
|
+
parts.push(colorize(c.prayer, "\u{1F54C} Asr 15:32") + " \x1b[2m(2h15)\x1b[0m");
|
|
39
|
+
}
|
|
29
40
|
console.log("\n Preview: " + parts.join(" \x1b[2m\u{2502}\x1b[0m ") + "\n");
|
|
30
41
|
}
|
|
42
|
+
function autoSave(config) {
|
|
43
|
+
if (config.wizard.autosave) {
|
|
44
|
+
saveConfig(config);
|
|
45
|
+
console.log(" \x1b[2m(autosaved)\x1b[0m");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
31
48
|
async function visibilityMenu(config) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
try {
|
|
50
|
+
const choices = [
|
|
51
|
+
{ name: "Model indicator", value: "model", checked: config.show.model },
|
|
52
|
+
{ name: "Context usage", value: "context", checked: config.show.context },
|
|
53
|
+
{ name: "5-hour usage limit", value: "usage5hr", checked: config.show.usage5hr },
|
|
54
|
+
{ name: "7-day usage limit", value: "usage7day", checked: config.show.usage7day },
|
|
55
|
+
{ name: "Budget delta arrows", value: "delta", checked: config.show.delta },
|
|
56
|
+
{ name: "Prayer times", value: "prayer", checked: config.show.prayer },
|
|
57
|
+
];
|
|
58
|
+
const selected = await checkbox({
|
|
59
|
+
message: "Toggle items (space=toggle, enter=confirm, ctrl+c=back)",
|
|
60
|
+
choices,
|
|
61
|
+
});
|
|
62
|
+
config.show.model = selected.includes("model");
|
|
63
|
+
config.show.context = selected.includes("context");
|
|
64
|
+
config.show.usage5hr = selected.includes("usage5hr");
|
|
65
|
+
config.show.usage7day = selected.includes("usage7day");
|
|
66
|
+
config.show.delta = selected.includes("delta");
|
|
67
|
+
config.show.prayer = selected.includes("prayer");
|
|
68
|
+
autoSave(config);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (isCancelled(e))
|
|
73
|
+
return true;
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
48
76
|
}
|
|
49
77
|
async function colorsMenu(config) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
try {
|
|
79
|
+
const colorOptions = [
|
|
80
|
+
{ name: `Model (current: ${colorize(config.colors.model, String(config.colors.model))})`, value: "model" },
|
|
81
|
+
{ name: `Context (current: ${colorize(config.colors.context, String(config.colors.context))})`, value: "context" },
|
|
82
|
+
{ name: `Context warning (current: ${colorize(config.colors.contextWarning, String(config.colors.contextWarning))})`, value: "contextWarning" },
|
|
83
|
+
{ name: `Usage (current: ${colorize(config.colors.usage, String(config.colors.usage))})`, value: "usage" },
|
|
84
|
+
{ name: `Delta under budget (current: ${colorize(config.colors.deltaUnder, String(config.colors.deltaUnder))})`, value: "deltaUnder" },
|
|
85
|
+
{ name: `Delta over budget (current: ${colorize(config.colors.deltaOver, String(config.colors.deltaOver))})`, value: "deltaOver" },
|
|
86
|
+
{ name: `Prayer (current: ${colorize(config.colors.prayer, String(config.colors.prayer))})`, value: "prayer" },
|
|
87
|
+
{ name: "\x1b[2mβ Back\x1b[0m", value: "back" },
|
|
88
|
+
];
|
|
89
|
+
while (true) {
|
|
90
|
+
const choice = await select({
|
|
91
|
+
message: "Select color to change (0-255)",
|
|
92
|
+
choices: colorOptions,
|
|
93
|
+
});
|
|
94
|
+
if (choice === "back")
|
|
95
|
+
return false;
|
|
96
|
+
const current = config.colors[choice];
|
|
97
|
+
const newValue = await input({
|
|
98
|
+
message: `Enter ANSI color code (0-255, current: ${colorize(current, String(current))})`,
|
|
99
|
+
default: String(current),
|
|
100
|
+
validate: (val) => {
|
|
101
|
+
const num = parseInt(val, 10);
|
|
102
|
+
if (isNaN(num) || num < 0 || num > 255)
|
|
103
|
+
return "Enter a number 0-255";
|
|
104
|
+
return true;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
config.colors[choice] = parseInt(newValue, 10);
|
|
108
|
+
autoSave(config);
|
|
109
|
+
// Update the menu option to show new color
|
|
110
|
+
const idx = colorOptions.findIndex(o => o.value === choice);
|
|
111
|
+
if (idx >= 0) {
|
|
112
|
+
const label = choice.replace(/([A-Z])/g, " $1").toLowerCase();
|
|
113
|
+
colorOptions[idx].name = `${label.charAt(0).toUpperCase() + label.slice(1)} (current: ${colorize(config.colors[choice], newValue)})`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
if (isCancelled(e))
|
|
119
|
+
return true;
|
|
120
|
+
throw e;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function prayerMenu(config) {
|
|
124
|
+
try {
|
|
125
|
+
const methodChoices = Object.entries(PRAYER_METHODS).map(([code, name]) => ({
|
|
126
|
+
name: `${name}${Number(code) === config.prayer.method ? " (current)" : ""}`,
|
|
127
|
+
value: code,
|
|
128
|
+
}));
|
|
129
|
+
methodChoices.push({ name: "\x1b[2mβ Back\x1b[0m", value: "back" });
|
|
130
|
+
while (true) {
|
|
131
|
+
const locationDisplay = config.prayer.location
|
|
132
|
+
? config.prayer.location.city
|
|
133
|
+
? `${config.prayer.location.city}, ${config.prayer.location.country}`
|
|
134
|
+
: `${config.prayer.location.latitude.toFixed(2)}, ${config.prayer.location.longitude.toFixed(2)}`
|
|
135
|
+
: "Auto-detect (IP)";
|
|
136
|
+
const choice = await select({
|
|
137
|
+
message: "Prayer settings",
|
|
138
|
+
choices: [
|
|
139
|
+
{ name: `Calculation method: ${PRAYER_METHODS[config.prayer.method]}`, value: "method" },
|
|
140
|
+
{ name: `Location: ${locationDisplay}`, value: "location" },
|
|
141
|
+
{ name: "\x1b[2mβ Back\x1b[0m", value: "back" },
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
if (choice === "back")
|
|
145
|
+
return false;
|
|
146
|
+
if (choice === "method") {
|
|
147
|
+
const method = await select({
|
|
148
|
+
message: "Select calculation method",
|
|
149
|
+
choices: methodChoices,
|
|
150
|
+
});
|
|
151
|
+
if (method !== "back") {
|
|
152
|
+
config.prayer.method = Number(method);
|
|
153
|
+
autoSave(config);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (choice === "location") {
|
|
157
|
+
const locChoice = await select({
|
|
158
|
+
message: "Location setting",
|
|
159
|
+
choices: [
|
|
160
|
+
{ name: "Auto-detect from IP (recommended)", value: "auto" },
|
|
161
|
+
{ name: "Enter city and country", value: "city" },
|
|
162
|
+
{ name: "Enter coordinates manually", value: "coords" },
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
if (locChoice === "auto") {
|
|
166
|
+
config.prayer.location = null;
|
|
167
|
+
autoSave(config);
|
|
168
|
+
}
|
|
169
|
+
else if (locChoice === "city") {
|
|
170
|
+
const city = await input({
|
|
171
|
+
message: "City name",
|
|
172
|
+
default: config.prayer.location?.city || "",
|
|
173
|
+
validate: (val) => val.trim() ? true : "Enter a city name",
|
|
174
|
+
});
|
|
175
|
+
const country = await input({
|
|
176
|
+
message: "Country",
|
|
177
|
+
default: config.prayer.location?.country || "",
|
|
178
|
+
validate: (val) => val.trim() ? true : "Enter a country",
|
|
179
|
+
});
|
|
180
|
+
console.log(" Looking up coordinates...");
|
|
181
|
+
const { geocodeCity } = await import("./prayer.js");
|
|
182
|
+
const location = await geocodeCity(city.trim(), country.trim());
|
|
183
|
+
if (location) {
|
|
184
|
+
config.prayer.location = location;
|
|
185
|
+
console.log(` Found: ${location.latitude.toFixed(4)}, ${location.longitude.toFixed(4)}\n`);
|
|
186
|
+
autoSave(config);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.log(" Could not find location. Please try again or enter coordinates manually.\n");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const lat = await input({
|
|
194
|
+
message: "Latitude",
|
|
195
|
+
default: config.prayer.location?.latitude?.toString() || "",
|
|
196
|
+
validate: (val) => {
|
|
197
|
+
const num = parseFloat(val);
|
|
198
|
+
if (isNaN(num) || num < -90 || num > 90)
|
|
199
|
+
return "Enter -90 to 90";
|
|
200
|
+
return true;
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
const lng = await input({
|
|
204
|
+
message: "Longitude",
|
|
205
|
+
default: config.prayer.location?.longitude?.toString() || "",
|
|
206
|
+
validate: (val) => {
|
|
207
|
+
const num = parseFloat(val);
|
|
208
|
+
if (isNaN(num) || num < -180 || num > 180)
|
|
209
|
+
return "Enter -180 to 180";
|
|
210
|
+
return true;
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
config.prayer.location = {
|
|
214
|
+
latitude: parseFloat(lat),
|
|
215
|
+
longitude: parseFloat(lng),
|
|
216
|
+
};
|
|
217
|
+
autoSave(config);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
if (isCancelled(e))
|
|
224
|
+
return true;
|
|
225
|
+
throw e;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function thresholdsMenu(config) {
|
|
229
|
+
try {
|
|
230
|
+
const contextWarning = await input({
|
|
231
|
+
message: "Context warning threshold % (turns orange)",
|
|
232
|
+
default: String(config.thresholds.contextWarning),
|
|
233
|
+
validate: (val) => {
|
|
234
|
+
const num = parseInt(val, 10);
|
|
235
|
+
if (isNaN(num) || num < 1 || num > 100)
|
|
236
|
+
return "Enter 1-100";
|
|
237
|
+
return true;
|
|
238
|
+
},
|
|
63
239
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
message: `Enter ANSI color code (0-255, current: ${colorize(current, String(current))})`,
|
|
69
|
-
default: String(current),
|
|
240
|
+
config.thresholds.contextWarning = parseInt(contextWarning, 10);
|
|
241
|
+
const cacheTtl = await input({
|
|
242
|
+
message: "API cache TTL (milliseconds)",
|
|
243
|
+
default: String(config.thresholds.cacheTtlMs),
|
|
70
244
|
validate: (val) => {
|
|
71
245
|
const num = parseInt(val, 10);
|
|
72
|
-
if (isNaN(num) || num < 0
|
|
73
|
-
return "Enter a number
|
|
246
|
+
if (isNaN(num) || num < 0)
|
|
247
|
+
return "Enter a positive number";
|
|
74
248
|
return true;
|
|
75
249
|
},
|
|
76
250
|
});
|
|
77
|
-
config.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (idx >= 0) {
|
|
81
|
-
const label = choice.replace(/([A-Z])/g, " $1").toLowerCase();
|
|
82
|
-
colorOptions[idx].name = `${label.charAt(0).toUpperCase() + label.slice(1)} (current: ${colorize(config.colors[choice], newValue)})`;
|
|
83
|
-
}
|
|
251
|
+
config.thresholds.cacheTtlMs = parseInt(cacheTtl, 10);
|
|
252
|
+
autoSave(config);
|
|
253
|
+
return false;
|
|
84
254
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const contextWarning = await input({
|
|
88
|
-
message: "Context warning threshold % (turns orange)",
|
|
89
|
-
default: String(config.thresholds.contextWarning),
|
|
90
|
-
validate: (val) => {
|
|
91
|
-
const num = parseInt(val, 10);
|
|
92
|
-
if (isNaN(num) || num < 1 || num > 100)
|
|
93
|
-
return "Enter 1-100";
|
|
255
|
+
catch (e) {
|
|
256
|
+
if (isCancelled(e))
|
|
94
257
|
return true;
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
258
|
+
throw e;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function settingsMenu(config) {
|
|
262
|
+
try {
|
|
263
|
+
const autosave = await confirm({
|
|
264
|
+
message: "Enable autosave? (saves after each change)",
|
|
265
|
+
default: config.wizard.autosave,
|
|
266
|
+
});
|
|
267
|
+
config.wizard.autosave = autosave;
|
|
268
|
+
saveConfig(config); // Always save settings changes
|
|
269
|
+
console.log(` Autosave ${autosave ? "enabled" : "disabled"}\n`);
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
if (isCancelled(e))
|
|
105
274
|
return true;
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
config.thresholds.cacheTtlMs = parseInt(cacheTtl, 10);
|
|
275
|
+
throw e;
|
|
276
|
+
}
|
|
109
277
|
}
|
|
110
278
|
export async function runWizard() {
|
|
111
|
-
console.log("\n\x1b[1mcc-statusline Configuration\x1b[0m
|
|
279
|
+
console.log("\n\x1b[1mcc-statusline Configuration\x1b[0m");
|
|
280
|
+
console.log("\x1b[2mCtrl+C to go back/exit\x1b[0m\n");
|
|
112
281
|
const config = loadConfig();
|
|
113
282
|
showPreview(config);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
showPreview(config);
|
|
134
|
-
break;
|
|
135
|
-
case "thresholds":
|
|
136
|
-
await thresholdsMenu(config);
|
|
137
|
-
break;
|
|
138
|
-
case "reset":
|
|
139
|
-
const confirmReset = await confirm({
|
|
140
|
-
message: "Reset all settings to defaults?",
|
|
141
|
-
default: false,
|
|
142
|
-
});
|
|
143
|
-
if (confirmReset) {
|
|
144
|
-
Object.assign(config, JSON.parse(JSON.stringify(DEFAULT_CONFIG)));
|
|
145
|
-
console.log(" Reset to defaults.\n");
|
|
283
|
+
try {
|
|
284
|
+
while (true) {
|
|
285
|
+
const autosaveLabel = config.wizard.autosave ? "\x1b[32mβ\x1b[0m" : "\x1b[2mβ\x1b[0m";
|
|
286
|
+
const choice = await select({
|
|
287
|
+
message: "What would you like to configure?",
|
|
288
|
+
choices: [
|
|
289
|
+
{ name: "Visibility (show/hide items)", value: "visibility" },
|
|
290
|
+
{ name: "Colors", value: "colors" },
|
|
291
|
+
{ name: "Thresholds", value: "thresholds" },
|
|
292
|
+
{ name: "Prayer settings", value: "prayer" },
|
|
293
|
+
{ name: `Settings ${autosaveLabel} autosave`, value: "settings" },
|
|
294
|
+
{ name: "Reset to defaults", value: "reset" },
|
|
295
|
+
...(config.wizard.autosave ? [] : [{ name: "Save & Exit", value: "save" }]),
|
|
296
|
+
{ name: config.wizard.autosave ? "Exit" : "Exit without saving", value: "exit" },
|
|
297
|
+
],
|
|
298
|
+
});
|
|
299
|
+
switch (choice) {
|
|
300
|
+
case "visibility":
|
|
301
|
+
await visibilityMenu(config);
|
|
146
302
|
showPreview(config);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
303
|
+
break;
|
|
304
|
+
case "colors":
|
|
305
|
+
await colorsMenu(config);
|
|
306
|
+
showPreview(config);
|
|
307
|
+
break;
|
|
308
|
+
case "thresholds":
|
|
309
|
+
await thresholdsMenu(config);
|
|
310
|
+
break;
|
|
311
|
+
case "prayer":
|
|
312
|
+
await prayerMenu(config);
|
|
313
|
+
break;
|
|
314
|
+
case "settings":
|
|
315
|
+
await settingsMenu(config);
|
|
316
|
+
break;
|
|
317
|
+
case "reset":
|
|
318
|
+
const confirmReset = await confirm({
|
|
319
|
+
message: "Reset all settings to defaults?",
|
|
320
|
+
default: false,
|
|
321
|
+
});
|
|
322
|
+
if (confirmReset) {
|
|
323
|
+
Object.assign(config, JSON.parse(JSON.stringify(DEFAULT_CONFIG)));
|
|
324
|
+
console.log(" Reset to defaults.\n");
|
|
325
|
+
autoSave(config);
|
|
326
|
+
showPreview(config);
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
case "save":
|
|
330
|
+
saveConfig(config);
|
|
331
|
+
console.log(`\n Saved to ${CONFIG_PATH}\n`);
|
|
159
332
|
return;
|
|
160
|
-
|
|
333
|
+
case "exit":
|
|
334
|
+
if (config.wizard.autosave) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const confirmExit = await confirm({
|
|
338
|
+
message: "Exit without saving changes?",
|
|
339
|
+
default: false,
|
|
340
|
+
});
|
|
341
|
+
if (confirmExit)
|
|
342
|
+
return;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (e) {
|
|
348
|
+
if (isCancelled(e)) {
|
|
349
|
+
if (config.wizard.autosave) {
|
|
350
|
+
console.log("\n \x1b[2mExited (changes autosaved)\x1b[0m\n");
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
console.log("\n \x1b[2mExited without saving\x1b[0m\n");
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
161
356
|
}
|
|
357
|
+
throw e;
|
|
162
358
|
}
|
|
163
359
|
}
|