@naarang/glancebar 1.0.2 → 1.0.4

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.
Files changed (3) hide show
  1. package/README.md +53 -10
  2. package/package.json +1 -1
  3. package/src/cli.ts +353 -15
package/README.md CHANGED
@@ -7,12 +7,14 @@ A customizable statusline for [Claude Code](https://claude.com/product/claude-co
7
7
 
8
8
  ## Features
9
9
 
10
- - Display upcoming calendar events from multiple Google accounts
11
- - Color-coded events per account
12
- - Countdown display for imminent events
13
- - Water break reminders to stay hydrated
14
- - Fully configurable via CLI
15
- - Cross-platform support (Windows, macOS, Linux)
10
+ - **Session info** - Project name, git branch, model, cost, lines changed, and context usage
11
+ - **System stats** - CPU and memory usage (optional)
12
+ - **Calendar events** - Upcoming events from multiple Google accounts
13
+ - **Meeting warnings** - Red alert when a meeting is 5 minutes away
14
+ - **Health reminders** - Water, stretch, and eye break reminders
15
+ - **Color-coded** - Everything has distinct colors for quick scanning
16
+ - **Fully configurable** via CLI
17
+ - **Cross-platform** support (Windows, macOS, Linux)
16
18
 
17
19
  ## Requirements
18
20
 
@@ -138,8 +140,14 @@ glancebar config --max-title 80
138
140
  # Toggle calendar name display
139
141
  glancebar config --show-calendar false
140
142
 
141
- # Enable/disable water reminders
143
+ # Enable/disable health reminders
142
144
  glancebar config --water-reminder true
145
+ glancebar config --stretch-reminder true
146
+ glancebar config --eye-reminder true
147
+
148
+ # Enable/disable system stats
149
+ glancebar config --cpu-usage true
150
+ glancebar config --memory-usage true
143
151
 
144
152
  # Reset to defaults
145
153
  glancebar config --reset
@@ -147,15 +155,46 @@ glancebar config --reset
147
155
 
148
156
  ## Display Format
149
157
 
158
+ Example output:
159
+ ```
160
+ glancebar | main* | Opus | $0.12 | +156 -23 | 9.7k/200k (5%) | In 15m: Team Standup (work)
161
+ ```
162
+
163
+ ### Session Info (from Claude Code)
164
+
165
+ | Field | Color | Example |
166
+ |-------|-------|---------|
167
+ | Project name | Blue | `glancebar`, `my-app` |
168
+ | Git branch | Magenta | `main`, `feature-x*` (asterisk = uncommitted changes) |
169
+ | Model name | Yellow | `Opus`, `Sonnet` |
170
+ | Cost | Green | `$0.01`, `$0.1234` |
171
+ | Lines changed | Green/Red | `+156 -23` |
172
+ | Context usage | Green/Yellow/Red | `9.7k/200k (5%)` |
173
+ | CPU usage | Green/Yellow/Red | `CPU 12%` |
174
+ | Memory usage | Green/Yellow/Red | `Mem 8.2/16.0GB` |
175
+
176
+ Context usage color changes based on percentage:
177
+ - **Green**: < 50%
178
+ - **Yellow**: 50-80%
179
+ - **Red**: > 80%
180
+
181
+ ### Calendar Events
182
+
150
183
  | State | Format | Example |
151
184
  |-------|--------|---------|
185
+ | **Meeting warning** | Red alert when ≤5m away | `Meeting in 3m - wrap up!` |
152
186
  | Upcoming (within threshold) | `In Xm: Title (account)` | `In 15m: Team Standup (work)` |
153
187
  | Current | `Now: Title (account)` | `Now: Team Standup (work)` |
154
188
  | Later | `HH:MM AM/PM: Title (account)` | `2:30 PM: Meeting (work)` |
155
189
  | No events | `No upcoming events` | |
156
- | Water reminder | Random hydration message | `Stay hydrated! Drink some water` |
157
190
 
158
- Events are color-coded by account (cyan, magenta, green, orange, blue, pink, yellow, purple).
191
+ ### Health Reminders (~30% chance)
192
+
193
+ | Type | Color | Example |
194
+ |------|-------|---------|
195
+ | Water | Cyan | `Stay hydrated! Drink some water` |
196
+ | Stretch | Green | `Time to stretch! Stand up and move` |
197
+ | Eye break | Magenta | `Eye break! Look 20ft away for 20s` |
159
198
 
160
199
  ## Configuration
161
200
 
@@ -177,7 +216,11 @@ All configuration is stored in `~/.glancebar/`:
177
216
  | `countdownThresholdMinutes` | 60 | Minutes threshold for countdown display |
178
217
  | `maxTitleLength` | 120 | Maximum event title length |
179
218
  | `showCalendarName` | true | Show account name after event |
180
- | `waterReminderEnabled` | true | Enable random water break reminders (~30% chance) |
219
+ | `waterReminderEnabled` | true | Enable random water break reminders |
220
+ | `stretchReminderEnabled` | true | Enable random stretch/posture reminders |
221
+ | `eyeReminderEnabled` | true | Enable random eye break reminders (20-20-20 rule) |
222
+ | `showCpuUsage` | false | Show CPU usage percentage |
223
+ | `showMemoryUsage` | false | Show memory usage |
181
224
 
182
225
  ## Building from Source
183
226
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naarang/glancebar",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "A customizable statusline for Claude Code - display calendar events, tasks, and more at a glance",
5
5
  "author": "Vishal Dubey",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -16,6 +16,10 @@ interface Config {
16
16
  countdownThresholdMinutes: number;
17
17
  maxTitleLength: number;
18
18
  waterReminderEnabled: boolean;
19
+ stretchReminderEnabled: boolean;
20
+ eyeReminderEnabled: boolean;
21
+ showCpuUsage: boolean;
22
+ showMemoryUsage: boolean;
19
23
  }
20
24
 
21
25
  const COLORS: Record<string, string> = {
@@ -47,6 +51,10 @@ const DEFAULT_CONFIG: Config = {
47
51
  countdownThresholdMinutes: 60,
48
52
  maxTitleLength: 120,
49
53
  waterReminderEnabled: true,
54
+ stretchReminderEnabled: true,
55
+ eyeReminderEnabled: true,
56
+ showCpuUsage: false,
57
+ showMemoryUsage: false,
50
58
  };
51
59
 
52
60
  const WATER_REMINDERS = [
@@ -60,6 +68,26 @@ const WATER_REMINDERS = [
60
68
  "Quick reminder: Drink water!",
61
69
  ];
62
70
 
71
+ const STRETCH_REMINDERS = [
72
+ "Time to stretch! Stand up and move",
73
+ "Stretch break! Roll your shoulders",
74
+ "Stand up and stretch your legs",
75
+ "Posture check! Sit up straight",
76
+ "Take a quick stretch break",
77
+ "Move your body! Quick stretch",
78
+ "Stretch your neck and shoulders",
79
+ "Stand up! Your body will thank you",
80
+ ];
81
+
82
+ const EYE_REMINDERS = [
83
+ "Eye break! Look 20ft away for 20s",
84
+ "Rest your eyes - look at something distant",
85
+ "20-20-20: Look away from screen",
86
+ "Give your eyes a break!",
87
+ "Look away from the screen for a moment",
88
+ "Eye rest time! Focus on something far",
89
+ ];
90
+
63
91
  function getConfigDir(): string {
64
92
  const home = process.env.HOME || process.env.USERPROFILE || "";
65
93
  return join(home, ".glancebar");
@@ -410,6 +438,75 @@ function formatCountdown(minutes: number): string {
410
438
  return mins === 0 ? `In ${hours}h` : `In ${hours}h${mins}m`;
411
439
  }
412
440
 
441
+ function getMeetingWarning(event: CalendarEvent | null): string | null {
442
+ if (!event) return null;
443
+
444
+ const now = new Date();
445
+ const minutesUntil = Math.round((event.start.getTime() - now.getTime()) / 60000);
446
+
447
+ // Warning when meeting is 5 minutes or less away
448
+ if (minutesUntil > 0 && minutesUntil <= 5) {
449
+ return `${COLORS.brightRed}Meeting in ${minutesUntil}m - wrap up!${COLORS.reset}`;
450
+ }
451
+
452
+ return null;
453
+ }
454
+
455
+ // ============================================================================
456
+ // System Stats
457
+ // ============================================================================
458
+
459
+ function getCpuUsage(): string | null {
460
+ try {
461
+ const os = require("os");
462
+ const cpus = os.cpus();
463
+
464
+ let totalIdle = 0;
465
+ let totalTick = 0;
466
+
467
+ for (const cpu of cpus) {
468
+ for (const type in cpu.times) {
469
+ totalTick += cpu.times[type as keyof typeof cpu.times];
470
+ }
471
+ totalIdle += cpu.times.idle;
472
+ }
473
+
474
+ const usage = Math.round(100 - (totalIdle / totalTick) * 100);
475
+
476
+ // Color based on usage
477
+ let color = COLORS.green;
478
+ if (usage >= 80) color = COLORS.red;
479
+ else if (usage >= 50) color = COLORS.yellow;
480
+
481
+ return `${color}CPU ${usage}%${COLORS.reset}`;
482
+ } catch {
483
+ return null;
484
+ }
485
+ }
486
+
487
+ function getMemoryUsage(): string | null {
488
+ try {
489
+ const os = require("os");
490
+ const totalMem = os.totalmem();
491
+ const freeMem = os.freemem();
492
+ const usedMem = totalMem - freeMem;
493
+ const usagePercent = Math.round((usedMem / totalMem) * 100);
494
+
495
+ // Format used memory
496
+ const usedGB = (usedMem / (1024 * 1024 * 1024)).toFixed(1);
497
+ const totalGB = (totalMem / (1024 * 1024 * 1024)).toFixed(1);
498
+
499
+ // Color based on usage
500
+ let color = COLORS.green;
501
+ if (usagePercent >= 80) color = COLORS.red;
502
+ else if (usagePercent >= 50) color = COLORS.yellow;
503
+
504
+ return `${color}Mem ${usedGB}/${totalGB}GB${COLORS.reset}`;
505
+ } catch {
506
+ return null;
507
+ }
508
+ }
509
+
413
510
  function formatTime(date: Date): string {
414
511
  const hours = date.getHours();
415
512
  const minutes = date.getMinutes();
@@ -440,13 +537,17 @@ Usage:
440
537
  glancebar config --max-title <length> Set max title length (default: 120)
441
538
  glancebar config --show-calendar <true|false> Show calendar name (default: true)
442
539
  glancebar config --water-reminder <true|false> Enable/disable water reminders (default: true)
540
+ glancebar config --stretch-reminder <true|false> Enable/disable stretch reminders (default: true)
541
+ glancebar config --eye-reminder <true|false> Enable/disable eye break reminders (default: true)
542
+ glancebar config --cpu-usage <true|false> Show CPU usage (default: false)
543
+ glancebar config --memory-usage <true|false> Show memory usage (default: false)
443
544
  glancebar config --reset Reset to default configuration
444
545
  glancebar setup Show setup instructions
445
546
 
446
547
  Examples:
447
548
  glancebar auth --add user@gmail.com
448
549
  glancebar config --lookahead 12
449
- glancebar config --water-reminder true
550
+ glancebar config --stretch-reminder false
450
551
 
451
552
  Config location: ${getConfigDir()}
452
553
  `);
@@ -681,6 +782,62 @@ function handleConfig(args: string[]) {
681
782
  return;
682
783
  }
683
784
 
785
+ // Handle --stretch-reminder
786
+ const stretchReminderIndex = args.indexOf("--stretch-reminder");
787
+ if (stretchReminderIndex !== -1) {
788
+ const value = args[stretchReminderIndex + 1]?.toLowerCase();
789
+ if (value !== "true" && value !== "false") {
790
+ console.error("Error: --stretch-reminder must be 'true' or 'false'");
791
+ process.exit(1);
792
+ }
793
+ config.stretchReminderEnabled = value === "true";
794
+ saveConfig(config);
795
+ console.log(`Stretch reminder ${value === "true" ? "enabled" : "disabled"}`);
796
+ return;
797
+ }
798
+
799
+ // Handle --eye-reminder
800
+ const eyeReminderIndex = args.indexOf("--eye-reminder");
801
+ if (eyeReminderIndex !== -1) {
802
+ const value = args[eyeReminderIndex + 1]?.toLowerCase();
803
+ if (value !== "true" && value !== "false") {
804
+ console.error("Error: --eye-reminder must be 'true' or 'false'");
805
+ process.exit(1);
806
+ }
807
+ config.eyeReminderEnabled = value === "true";
808
+ saveConfig(config);
809
+ console.log(`Eye break reminder ${value === "true" ? "enabled" : "disabled"}`);
810
+ return;
811
+ }
812
+
813
+ // Handle --cpu-usage
814
+ const cpuUsageIndex = args.indexOf("--cpu-usage");
815
+ if (cpuUsageIndex !== -1) {
816
+ const value = args[cpuUsageIndex + 1]?.toLowerCase();
817
+ if (value !== "true" && value !== "false") {
818
+ console.error("Error: --cpu-usage must be 'true' or 'false'");
819
+ process.exit(1);
820
+ }
821
+ config.showCpuUsage = value === "true";
822
+ saveConfig(config);
823
+ console.log(`CPU usage display ${value === "true" ? "enabled" : "disabled"}`);
824
+ return;
825
+ }
826
+
827
+ // Handle --memory-usage
828
+ const memoryUsageIndex = args.indexOf("--memory-usage");
829
+ if (memoryUsageIndex !== -1) {
830
+ const value = args[memoryUsageIndex + 1]?.toLowerCase();
831
+ if (value !== "true" && value !== "false") {
832
+ console.error("Error: --memory-usage must be 'true' or 'false'");
833
+ process.exit(1);
834
+ }
835
+ config.showMemoryUsage = value === "true";
836
+ saveConfig(config);
837
+ console.log(`Memory usage display ${value === "true" ? "enabled" : "disabled"}`);
838
+ return;
839
+ }
840
+
684
841
  // Show current config
685
842
  console.log(`
686
843
  Glancebar Configuration
@@ -696,34 +853,209 @@ Calendar Settings:
696
853
 
697
854
  Reminders:
698
855
  Water reminder: ${config.waterReminderEnabled ? "enabled" : "disabled"}
856
+ Stretch reminder: ${config.stretchReminderEnabled ? "enabled" : "disabled"}
857
+ Eye break reminder: ${config.eyeReminderEnabled ? "enabled" : "disabled"}
858
+
859
+ System Stats:
860
+ CPU usage: ${config.showCpuUsage ? "enabled" : "disabled"}
861
+ Memory usage: ${config.showMemoryUsage ? "enabled" : "disabled"}
699
862
  `);
700
863
  }
701
864
 
702
- function shouldShowWaterReminder(config: Config): boolean {
703
- if (!config.waterReminderEnabled) return false;
865
+ function getRandomReminder(config: Config): string | null {
866
+ const enabledReminders: Array<() => string> = [];
867
+
868
+ if (config.waterReminderEnabled) {
869
+ enabledReminders.push(() => {
870
+ const reminder = WATER_REMINDERS[Math.floor(Math.random() * WATER_REMINDERS.length)];
871
+ return `${COLORS.brightCyan}${reminder}${COLORS.reset}`;
872
+ });
873
+ }
874
+
875
+ if (config.stretchReminderEnabled) {
876
+ enabledReminders.push(() => {
877
+ const reminder = STRETCH_REMINDERS[Math.floor(Math.random() * STRETCH_REMINDERS.length)];
878
+ return `${COLORS.brightGreen}${reminder}${COLORS.reset}`;
879
+ });
880
+ }
881
+
882
+ if (config.eyeReminderEnabled) {
883
+ enabledReminders.push(() => {
884
+ const reminder = EYE_REMINDERS[Math.floor(Math.random() * EYE_REMINDERS.length)];
885
+ return `${COLORS.brightMagenta}${reminder}${COLORS.reset}`;
886
+ });
887
+ }
704
888
 
705
- // ~30% chance to show water reminder
706
- return Math.random() < 0.3;
889
+ if (enabledReminders.length === 0) return null;
890
+
891
+ // ~30% chance to show any reminder
892
+ if (Math.random() >= 0.3) return null;
893
+
894
+ // Pick a random reminder type from enabled ones
895
+ const randomPicker = enabledReminders[Math.floor(Math.random() * enabledReminders.length)];
896
+ return randomPicker();
707
897
  }
708
898
 
709
- function getWaterReminder(): string {
710
- const reminder = WATER_REMINDERS[Math.floor(Math.random() * WATER_REMINDERS.length)];
711
- return `${COLORS.brightCyan}${reminder}${COLORS.reset}`;
899
+ interface ClaudeCodeStatus {
900
+ model?: { display_name?: string };
901
+ cost?: {
902
+ total_cost_usd?: number;
903
+ total_lines_added?: number;
904
+ total_lines_removed?: number;
905
+ };
906
+ cwd?: string;
907
+ workspace?: {
908
+ project_dir?: string;
909
+ };
910
+ context_window?: {
911
+ context_window_size?: number;
912
+ current_usage?: {
913
+ input_tokens?: number;
914
+ output_tokens?: number;
915
+ cache_creation_input_tokens?: number;
916
+ cache_read_input_tokens?: number;
917
+ };
918
+ };
712
919
  }
713
920
 
714
- async function outputStatusline() {
715
- // Consume stdin (Claude Code sends JSON)
921
+ function getGitBranch(cwd?: string): string | null {
716
922
  try {
717
- for await (const _ of Bun.stdin.stream()) break;
718
- } catch {}
923
+ const { execSync } = require("child_process");
924
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
925
+ cwd: cwd || process.cwd(),
926
+ encoding: "utf-8",
927
+ stdio: ["pipe", "pipe", "pipe"],
928
+ }).trim();
929
+
930
+ // Check if there are uncommitted changes
931
+ let isDirty = false;
932
+ try {
933
+ const status = execSync("git status --porcelain", {
934
+ cwd: cwd || process.cwd(),
935
+ encoding: "utf-8",
936
+ stdio: ["pipe", "pipe", "pipe"],
937
+ }).trim();
938
+ isDirty = status.length > 0;
939
+ } catch {}
940
+
941
+ return isDirty ? `${branch}*` : branch;
942
+ } catch {
943
+ return null;
944
+ }
945
+ }
946
+
947
+ function getProjectName(projectDir?: string): string | null {
948
+ if (!projectDir) return null;
949
+ // Extract the last part of the path as project name
950
+ const parts = projectDir.replace(/\\/g, "/").split("/").filter(Boolean);
951
+ return parts.length > 0 ? parts[parts.length - 1] : null;
952
+ }
953
+
954
+ async function readStdinJson(): Promise<ClaudeCodeStatus | null> {
955
+ try {
956
+ const chunks: Uint8Array[] = [];
957
+ for await (const chunk of Bun.stdin.stream()) {
958
+ chunks.push(chunk);
959
+ }
960
+ if (chunks.length === 0) return null;
961
+ const text = Buffer.concat(chunks).toString("utf-8").trim();
962
+ if (!text) return null;
963
+ return JSON.parse(text);
964
+ } catch {
965
+ return null;
966
+ }
967
+ }
968
+
969
+ function formatSessionInfo(status: ClaudeCodeStatus): string {
970
+ const parts: string[] = [];
971
+
972
+ // Project name
973
+ const projectName = getProjectName(status.workspace?.project_dir);
974
+ if (projectName) {
975
+ parts.push(`${COLORS.brightBlue}${projectName}${COLORS.reset}`);
976
+ }
977
+
978
+ // Git branch
979
+ const gitBranch = getGitBranch(status.cwd || status.workspace?.project_dir);
980
+ if (gitBranch) {
981
+ parts.push(`${COLORS.magenta}${gitBranch}${COLORS.reset}`);
982
+ }
983
+
984
+ // Model name
985
+ if (status.model?.display_name) {
986
+ parts.push(`${COLORS.brightYellow}${status.model.display_name}${COLORS.reset}`);
987
+ }
988
+
989
+ // Cost
990
+ if (status.cost?.total_cost_usd !== undefined) {
991
+ const cost = status.cost.total_cost_usd;
992
+ const costStr = cost < 0.01 ? `$${cost.toFixed(4)}` : `$${cost.toFixed(2)}`;
993
+ parts.push(`${COLORS.green}${costStr}${COLORS.reset}`);
994
+ }
995
+
996
+ // Lines changed
997
+ const linesAdded = status.cost?.total_lines_added || 0;
998
+ const linesRemoved = status.cost?.total_lines_removed || 0;
999
+ if (linesAdded > 0 || linesRemoved > 0) {
1000
+ const linesStr = `${COLORS.green}+${linesAdded}${COLORS.reset} ${COLORS.red}-${linesRemoved}${COLORS.reset}`;
1001
+ parts.push(linesStr);
1002
+ }
1003
+
1004
+ // Context usage (using current_usage for accurate context window state)
1005
+ if (status.context_window?.current_usage && status.context_window?.context_window_size) {
1006
+ const usage = status.context_window.current_usage;
1007
+ // Sum all token types for total context usage
1008
+ const totalUsed =
1009
+ (usage.input_tokens || 0) +
1010
+ (usage.output_tokens || 0) +
1011
+ (usage.cache_creation_input_tokens || 0) +
1012
+ (usage.cache_read_input_tokens || 0);
1013
+ const windowSize = status.context_window.context_window_size;
1014
+ const percentage = Math.round((totalUsed / windowSize) * 100);
1015
+ const usedK = (totalUsed / 1000).toFixed(1);
1016
+ const windowK = Math.round(windowSize / 1000);
1017
+
1018
+ // Color based on usage: green < 50%, yellow 50-80%, red > 80%
1019
+ let color = COLORS.green;
1020
+ if (percentage >= 80) color = COLORS.red;
1021
+ else if (percentage >= 50) color = COLORS.yellow;
1022
+
1023
+ parts.push(`${color}${usedK}k/${windowK}k (${percentage}%)${COLORS.reset}`);
1024
+ }
1025
+
1026
+ return parts.join(" | ");
1027
+ }
1028
+
1029
+ async function outputStatusline() {
1030
+ // Read and parse stdin from Claude Code
1031
+ const status = await readStdinJson();
719
1032
 
720
1033
  try {
721
1034
  const config = loadConfig();
722
1035
  const parts: string[] = [];
723
1036
 
724
- // Check for water reminder first
725
- if (shouldShowWaterReminder(config)) {
726
- parts.push(getWaterReminder());
1037
+ // Add session info from Claude Code
1038
+ if (status) {
1039
+ const sessionInfo = formatSessionInfo(status);
1040
+ if (sessionInfo) {
1041
+ parts.push(sessionInfo);
1042
+ }
1043
+ }
1044
+
1045
+ // Add system stats if enabled
1046
+ if (config.showCpuUsage) {
1047
+ const cpu = getCpuUsage();
1048
+ if (cpu) parts.push(cpu);
1049
+ }
1050
+ if (config.showMemoryUsage) {
1051
+ const mem = getMemoryUsage();
1052
+ if (mem) parts.push(mem);
1053
+ }
1054
+
1055
+ // Check for health reminder (water, stretch, eye break)
1056
+ const reminder = getRandomReminder(config);
1057
+ if (reminder) {
1058
+ parts.push(reminder);
727
1059
  }
728
1060
 
729
1061
  // Get calendar events
@@ -731,6 +1063,12 @@ async function outputStatusline() {
731
1063
  const events = await getUpcomingEvents(config);
732
1064
  const event = getCurrentOrNextEvent(events);
733
1065
 
1066
+ // Check for meeting warning (within 5 minutes)
1067
+ const meetingWarning = getMeetingWarning(event);
1068
+ if (meetingWarning) {
1069
+ parts.push(meetingWarning);
1070
+ }
1071
+
734
1072
  if (event) {
735
1073
  parts.push(formatEvent(event, config));
736
1074
  } else if (parts.length === 0) {