@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 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
+ }
@@ -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
- const choices = [
33
- { name: "Model indicator", value: "model", checked: config.show.model },
34
- { name: "Context usage", value: "context", checked: config.show.context },
35
- { name: "5-hour usage limit", value: "usage5hr", checked: config.show.usage5hr },
36
- { name: "7-day usage limit", value: "usage7day", checked: config.show.usage7day },
37
- { name: "Budget delta arrows", value: "delta", checked: config.show.delta },
38
- ];
39
- const selected = await checkbox({
40
- message: "Toggle items to show (space to toggle, enter to confirm)",
41
- choices,
42
- });
43
- config.show.model = selected.includes("model");
44
- config.show.context = selected.includes("context");
45
- config.show.usage5hr = selected.includes("usage5hr");
46
- config.show.usage7day = selected.includes("usage7day");
47
- config.show.delta = selected.includes("delta");
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
- const colorOptions = [
51
- { name: `Model (current: ${colorize(config.colors.model, String(config.colors.model))})`, value: "model" },
52
- { name: `Context (current: ${colorize(config.colors.context, String(config.colors.context))})`, value: "context" },
53
- { name: `Context warning (current: ${colorize(config.colors.contextWarning, String(config.colors.contextWarning))})`, value: "contextWarning" },
54
- { name: `Usage (current: ${colorize(config.colors.usage, String(config.colors.usage))})`, value: "usage" },
55
- { name: `Delta under budget (current: ${colorize(config.colors.deltaUnder, String(config.colors.deltaUnder))})`, value: "deltaUnder" },
56
- { name: `Delta over budget (current: ${colorize(config.colors.deltaOver, String(config.colors.deltaOver))})`, value: "deltaOver" },
57
- { name: "Back", value: "back" },
58
- ];
59
- while (true) {
60
- const choice = await select({
61
- message: "Select color to change (ANSI 256 codes: 0-255)",
62
- choices: colorOptions,
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
- if (choice === "back")
65
- break;
66
- const current = config.colors[choice];
67
- const newValue = await input({
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 || num > 255)
73
- return "Enter a number 0-255";
246
+ if (isNaN(num) || num < 0)
247
+ return "Enter a positive number";
74
248
  return true;
75
249
  },
76
250
  });
77
- config.colors[choice] = parseInt(newValue, 10);
78
- // Update the menu option to show new color
79
- const idx = colorOptions.findIndex(o => o.value === choice);
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
- async function thresholdsMenu(config) {
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
- config.thresholds.contextWarning = parseInt(contextWarning, 10);
98
- const cacheTtl = await input({
99
- message: "API cache TTL (milliseconds)",
100
- default: String(config.thresholds.cacheTtlMs),
101
- validate: (val) => {
102
- const num = parseInt(val, 10);
103
- if (isNaN(num) || num < 0)
104
- return "Enter a positive number";
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\n");
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
- while (true) {
115
- const choice = await select({
116
- message: "What would you like to configure?",
117
- choices: [
118
- { name: "Visibility (show/hide items)", value: "visibility" },
119
- { name: "Colors", value: "colors" },
120
- { name: "Thresholds", value: "thresholds" },
121
- { name: "Reset to defaults", value: "reset" },
122
- { name: "Save & Exit", value: "save" },
123
- { name: "Exit without saving", value: "exit" },
124
- ],
125
- });
126
- switch (choice) {
127
- case "visibility":
128
- await visibilityMenu(config);
129
- showPreview(config);
130
- break;
131
- case "colors":
132
- await colorsMenu(config);
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
- break;
149
- case "save":
150
- saveConfig(config);
151
- console.log(`\n Saved to ${CONFIG_PATH}\n`);
152
- return;
153
- case "exit":
154
- const confirmExit = await confirm({
155
- message: "Exit without saving changes?",
156
- default: false,
157
- });
158
- if (confirmExit)
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
- break;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daliovic/cc-statusline",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "Minimal Claude Code statusline with usage limits and budget tracking",
5
5
  "type": "module",
6
6
  "main": "dist/statusline.js",